├── .gitignore ├── README.md ├── data └── shakespeare-plays.csv ├── lib ├── bench.js ├── index.js └── search.js ├── native ├── Cargo.toml └── src │ └── lib.rs └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | native/Cargo.lock 2 | native/target 3 | native/index.node 4 | **/*~ 5 | **/node_modules 6 | **/.DS_Store 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wc-demo 2 | 3 | A simple word-counting demo to showcase [Neon](https://github.com/rustbridge/neon). 4 | 5 | # Setup 6 | 7 | See the [requirements for Neon](https://github.com/rustbridge/neon#requirements) (TL;DR: Node, Rust, and on OS X, XCode). 8 | 9 | # Build and run 10 | 11 | From the project root directory, run: 12 | 13 | ``` 14 | % npm install 15 | % node -e 'require("./")' 16 | ``` 17 | -------------------------------------------------------------------------------- /lib/bench.js: -------------------------------------------------------------------------------- 1 | var hrtime = process.hrtime; 2 | 3 | function Benchmark(result, diff) { 4 | this.result = result; 5 | this.elapsed = diff; 6 | this.sec = diff[0]; 7 | this.msec = Math.floor(diff[1] / 1000000); 8 | this.μsec = Math.floor((diff[1] - this.msec * 1000000) / 1000); 9 | this.nsec = diff[1] - (this.msec * 1000000) - (this.μsec * 1000); 10 | } 11 | 12 | Benchmark.of = function(thunk) { 13 | var time = hrtime(); 14 | var result = thunk(); 15 | var diff = hrtime(time); 16 | return new Benchmark(result, diff); 17 | }; 18 | 19 | Benchmark.prototype.toString = function() { 20 | var components = []; 21 | if (this.sec) { 22 | components.push(this.sec + "s"); 23 | } 24 | if (this.msec) { 25 | components.push(this.msec + "ms"); 26 | } 27 | if (this.μsec) { 28 | components.push(this.μsec + "μs"); 29 | } 30 | if (this.nsec) { 31 | components.push(this.nsec + "ns"); 32 | } 33 | return JSON.stringify(this.result) + " [" + components.join(", ") + "]"; 34 | }; 35 | 36 | Benchmark.prototype.inspect = Benchmark.prototype.toString; 37 | 38 | module.exports = Benchmark.of; 39 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var bench = require('./bench'); 4 | 5 | var wc = { 6 | js: require('./search'), 7 | neon: require('../native') 8 | }; 9 | 10 | var ROOT = path.resolve(__dirname, ".."); 11 | var DATA = path.resolve(ROOT, "data"); 12 | 13 | var string = fs.readFileSync(path.resolve(DATA, "shakespeare-plays.csv"), 'utf8'); 14 | var buffer = fs.readFileSync(path.resolve(DATA, "shakespeare-plays.csv")); 15 | 16 | console.log(bench(function() { return wc.js.search(string, "thee"); })); 17 | console.log(bench(function() { return wc.neon.search(buffer, "thee"); })); 18 | -------------------------------------------------------------------------------- /lib/search.js: -------------------------------------------------------------------------------- 1 | function lines(corpus) { 2 | return corpus.split(/\n+/) 3 | .map(function(line) { 4 | return line.substring(nthIndexOf(line, ",", 3) + 1); 5 | }); 6 | } 7 | 8 | function nthIndexOf(haystack, needle, n) { 9 | var index = -1; 10 | for (var i = 0; i < n; i++) { 11 | index = haystack.indexOf(needle, index + 1); 12 | if (index < 0) { 13 | return -1; 14 | } 15 | } 16 | return index; 17 | } 18 | 19 | function skipPunc(word) { 20 | for (var i = 0, n = word.length; i < n; i++) { 21 | if (/[a-zA-Z]/.test(word[i])) { 22 | break; 23 | } 24 | } 25 | return word.substring(i); 26 | } 27 | 28 | function matches(word, search) { 29 | var start = skipPunc(word); 30 | var i = 0, m = start.length, n = search.length; 31 | if (m < n) { 32 | return false; 33 | } 34 | while (i < n) { 35 | if (start[i].toLowerCase() !== search[i]) { 36 | return false; 37 | } 38 | i++; 39 | } 40 | return i == m || !/[a-zA-Z]/.test(start[i]); 41 | } 42 | 43 | function wcLine(line, search) { 44 | var words = line.split(' '); 45 | var total = 0; 46 | for (var i = 0, n = words.length; i < n; i++) { 47 | if (matches(words[i], search)) { 48 | total++; 49 | } 50 | } 51 | return total; 52 | } 53 | 54 | exports.search = function search(corpus, search) { 55 | var ls = lines(corpus); 56 | var total = 0; 57 | for (var i = 0, n = ls.length; i < n; i++) { 58 | total += wcLine(ls[i], search); 59 | } 60 | return total; 61 | } 62 | -------------------------------------------------------------------------------- /native/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wc-demo" 3 | version = "0.1.2" 4 | authors = ["Dave Herman "] 5 | license = "MIT" 6 | 7 | [lib] 8 | name = "wc_demo" 9 | crate-type = ["dylib"] 10 | 11 | [dependencies] 12 | neon = "0.1" 13 | rayon = { git = "https://github.com/nikomatsakis/rayon" } 14 | -------------------------------------------------------------------------------- /native/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate neon; 3 | extern crate rayon; 4 | 5 | use std::str; 6 | 7 | use rayon::par_iter::{ParallelIterator, IntoParallelIterator}; 8 | 9 | use neon::vm::{Call, JsResult, Lock}; 10 | use neon::js::{JsInteger, JsString}; 11 | use neon::js::binary::JsBuffer; 12 | use neon::mem::Handle; 13 | 14 | fn lines(corpus: &str) -> Vec<&str> { 15 | corpus.lines() 16 | .map(|line| { 17 | line.splitn(4, ',').nth(3).unwrap().trim() 18 | }) 19 | .collect() 20 | } 21 | 22 | fn matches(word: &str, search: &str) -> bool { 23 | let mut search = search.chars(); 24 | for ch in word.chars().skip_while(|ch| !ch.is_alphabetic()) { 25 | match search.next() { 26 | None => { return !ch.is_alphabetic(); } 27 | Some(expect) => { 28 | if ch.to_lowercase().next() != Some(expect) { 29 | return false; 30 | } 31 | } 32 | } 33 | } 34 | return search.next().is_none(); 35 | } 36 | 37 | fn wc_line(line: &str, search: &str) -> i32 { 38 | let mut total = 0; 39 | for word in line.split(' ') { 40 | if matches(word, search) { 41 | total += 1; 42 | } 43 | } 44 | total 45 | } 46 | 47 | // Also valid, with comparable performance: 48 | 49 | /* 50 | fn wc_line(line: &str, search: &str) -> i32 { 51 | line.split(' ') 52 | .filter(|word| matches(word, search)) 53 | .fold(0, |sum, _| sum + 1) 54 | } 55 | */ 56 | 57 | fn wc_sequential(lines: &Vec<&str>, search: &str) -> i32 { 58 | lines.into_iter() 59 | .map(|line| wc_line(line, search)) 60 | .fold(0, |sum, line| sum + line) 61 | } 62 | 63 | fn wc_parallel(lines: &Vec<&str>, search: &str) -> i32 { 64 | lines.into_par_iter() 65 | .map(|line| wc_line(line, search)) 66 | .sum() 67 | } 68 | 69 | fn search(call: Call) -> JsResult { 70 | let scope = call.scope; 71 | let mut buffer: Handle = try!(try!(call.arguments.require(scope, 0)).check::()); 72 | let string: Handle = try!(try!(call.arguments.require(scope, 1)).check::()); 73 | let search = &string.value()[..]; 74 | let total = buffer.grab(|data| { 75 | let corpus = str::from_utf8(data.as_slice()).ok().unwrap(); 76 | wc_parallel(&lines(corpus), search) 77 | }); 78 | Ok(JsInteger::new(scope, total)) 79 | } 80 | 81 | register_module!(m, { 82 | m.export("search", search) 83 | }); 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wc-demo", 3 | "version": "0.1.1", 4 | "description": "A word count demo to showcase Neon.", 5 | "main": "lib/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/dherman/wc-demo" 9 | }, 10 | "author": "Dave Herman ", 11 | "license": "MIT", 12 | "dependencies": { 13 | "neon-cli": "^0.1.6" 14 | }, 15 | "scripts": { 16 | "install": "neon build" 17 | } 18 | } 19 | --------------------------------------------------------------------------------