-
Notifications
You must be signed in to change notification settings - Fork 31
/
core-estimator.js
376 lines (311 loc) · 10.1 KB
/
core-estimator.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
/*
* Core Estimator
* CPU core estimation timing attack using web workers
* A polyfill for navigator.hardwareConcurrency
* 2014-05-27
*
* Copyright ΩF:∅ Working Group contributors
* License: MIT
* See LICENSE.md
*/
/*! @source https://github.com/oftn/core-estimator/blob/master/core-estimator.js */
"use strict";
(function(view) {
// Configuration (default: medium accuracy)
var SAMPLES = 20;
var WORKLOAD = 0x400000;
// A workload of 0x2000000 with 15-20 samples should give you medium-high accuracy
// at 6-8x the default runtime. Not suggested in production webapps!
var dom_implemented = navigator.hardwareConcurrency;
var doc = document;
// Get the path of the currently running script
var path = (doc.currentScript || doc.scripts[doc.scripts.length-1]).src
.replace(/\/[^\/]+$/, "/");
// Use PNaCl in Chrome
if (!dom_implemented && navigator.mimeTypes["application/x-pnacl"]) {
var HTML = "http://www.w3.org/1999/xhtml";
var log_error = console.error.bind(console);
var calls = [];
var on_message = function(event) {
var cores = navigator.hardwareConcurrency = event.data;
var call;
navigator.getHardwareConcurrency = function(callback, options) {
callback(cores);
if (options && options.progress) {
options.progress(cores, cores, cores);
}
};
while (call = calls.shift()) {
navigator.getHardwareConcurrency(call[0], call[1]);
}
// Cleanup
listener_div.removeEventListener("load", on_load, true);
listener_div.removeEventListener("message", on_message, true);
listener_div.removeEventListener("error", log_error, true);
listener_div.removeEventListener("crash", log_error, true);
doc.documentElement.removeChild(listener_div);
};
var on_load = function() {
embed.postMessage(0);
};
navigator.getHardwareConcurrency = function(callback, options) {
calls.push([callback, options]);
};
var listener_div = doc.createElementNS(HTML, "div");
listener_div.addEventListener("load", on_load, true);
listener_div.addEventListener("message", on_message, true);
listener_div.addEventListener("error", log_error, true);
listener_div.addEventListener("crash", log_error, true);
var embed = doc.createElementNS(HTML, "embed");
embed.setAttribute("path", path + "nacl_module/pnacl/Release");
embed.setAttribute("src", path + "nacl_module/pnacl/Release/cores.nmf");
embed.setAttribute("type", "application/x-pnacl");
listener_div.appendChild(embed);
doc.documentElement.appendChild(listener_div);
return;
}
// Set up performance testing function
var performance = view.performance || Date;
if (!performance.now) {
if (performance.webkitNow) {
performance.now = performance.webkitNow;
} else {
performance.now = function() { return +new Date; };
}
}
// Path to workload.js is derived from the path of the running script.
var workload = path + "workload.js";
var previously_run = false;
// Set navigator.hardwareConcurrency to a sane value before getHardwareConcurrency is ever run
if (!dom_implemented) {
/** @expose */ navigator.hardwareConcurrency = 1;
if (typeof Worker === "undefined") {
// Web workers not supported, effectively single-core
dom_implemented = true;
}
}
/**
* navigator.getHardwareConcurrency(callback)
*
* Performs the statistical test to determine the correct number of cores
* and calls its callback with the core number as its argument.
*
* @expose
**/
navigator.getHardwareConcurrency = function(callback, options) {
options = options || {};
if (!('use_cache' in options)) {
options.use_cache = true;
}
// If we already have an answer, return early.
if (dom_implemented || (options.use_cache && previously_run)) {
callback(navigator.hardwareConcurrency);
return;
}
doc.documentElement.style.cursor = "progress";
var workers = []; // An array of workers ready to run the payload
var worker_size = 1;
var control;
var controldata = [];
iterate(function(worker_size, report) {
measure(workers, worker_size, SAMPLES, function(data) {
if (worker_size === 1) {
Array.prototype.push.apply(controldata, data);
control = analyse(controldata);
report(true);
} else {
var group = analyse(data);
var gv_gs = group.uvariance / group.size;
var cv_cs = control.uvariance / control.size;
var tscore = (group.mean - control.mean) / Math.sqrt(gv_gs + cv_cs);
var freedom = Math.pow(gv_gs + cv_cs, 2) /
(Math.pow(group.uvariance, 2) / (Math.pow(group.size, 2) * (group.size - 1) ) +
Math.pow(control.uvariance, 2) / (Math.pow(control.size, 2) * (control.size - 1))); // don't ask
report(accept(tscore, freedom));
}
});
}, function(cores) {
// Terminate our workers, we don't need them anymore.
for (var i = 0, len = workers.length; i < len; i++) {
workers[i].terminate();
}
// We found an estimate
doc.documentElement.style.cursor = "";
navigator.hardwareConcurrency = cores;
previously_run = true;
callback(cores);
}, options.progress);
}
/**
* measure()
*
* Given a set of workers and a sample size,
* it calls back with an array of times it took
* to run all the workers simultaneously.
*
**/
function measure(workers, worker_size, sample_size, callback) {
var samples = [];
// Guarantee that we have enough workers
for (var i = workers.length; i < worker_size; i++) {
workers.push(new Worker(workload));
}
loop(function(_repeat) {
var begin, left = worker_size; // Number of workers we are waiting to finish
// When a worker completes
for (var i = 0; i < worker_size; i++) {
workers[i].onmessage = function() {
left--;
if (!left) {
sample_size--;
samples.push(performance.now() - begin);
if (sample_size) {
_repeat();
} else {
callback(samples);
}
}
}
}
// Kick-off our workers and start the clock
for (var i = 0; i < worker_size; i++) {
workers[i].postMessage(WORKLOAD);
}
begin = performance.now();
});
}
function loop(body) {
(function next() {
body(next);
}());
}
/**
* iterate(test, answer, progress)
*
* Given a test function and a callback,
* it will conduct a binary search to find the highest value
* which the test function returns as passing.
*
* Optionally takes a callback to report the state of the iterator.
*
**/
function iterate(test, answer, progress) {
// Let S be the set of possible core numbers on this machine.
// S = {x \in N | x != 0 }.
var min = 1, max = 1/0;
// Find an upper bound (max - 1) on S by testing powers of two.
// During these tests, we also come across a lower bound (min).
(function repeat(cores) {
if (progress) {
progress(min, max, cores);
}
test(1, function() {
test(cores, function(pass) {
if (pass) {
min = cores;
// Repeat the test with double the cores.
repeat(2 * cores);
} else {
max = cores;
// * If S has one element, we found the number
// * S has one element iff max - min = 1.
// * Given max = min * 2 in invariant of this test,
// S has one element iff min = 1.
if (min === 1) {
return answer(min);
}
// We have finally found our upper bound; search space.
search(min * 3 / 2, min / 4);
}
});
});
}(2));
function search(center, pivot) {
if (progress) {
progress(min, max, center);
}
test(1, function() {
test(center, function(pass) {
if (pass) {
min = center;
center += pivot;
} else {
max = center;
center -= pivot;
}
if (max - min === 1) {
return answer(min);
}
if (!pivot) {
// This means we haven't found an answer.
// Oh well. Answer with the upper bound.
return answer(max - 1);
}
search(center, pivot >> 1);
});
});
}
}
/**
* analyse(array)
*
* Given an array of values, it returns a set of statistics.
*
**/
function analyse(data) {
// If we have no values, return null.
var len = data.length;
if (!len) {
return null;
}
// Iterate through data, gathering information.
var min = 1/0, max = -1/0;
var sum = 0;
var sum_squared_datum = 0;
for (var i = 0; i < len; i++) {
var datum = data[i];
if (datum < min) min = datum;
if (datum > max) max = datum;
sum += datum;
sum_squared_datum += Math.pow(datum, 2);
}
// Calculate statistics from information.
var mean = sum / len;
var mean_squared = Math.pow(mean, 2);
var variance = 0;
var unbiased_variance = 0;
if (len > 1) {
variance = sum_squared_datum / len - mean_squared;
unbiased_variance = (sum_squared_datum - len * mean_squared) / (len - 1);
}
// Store statistics into object
var stats = {
size: len,
//min: min,
//max: max,
mean: mean,
//variance: variance,
uvariance: unbiased_variance
};
return stats;
};
/**
* accept(tscore, freedom)
*
* Given a t-score and the number of degrees of freedom,
* return a boolean indicating whether the tscore is less than the
* critical value found in the t-table.
*
**/
// This object is created from a t-table given a one-sided test and a 99.5% confidence.
/** @const */ var table = {1: 63.66, 2: 9.925, 3: 5.841, 4: 4.604, 5: 4.032, 6: 3.707, 7: 3.499, 8: 3.355, 9: 3.25, 10: 3.169, 11: 3.106, 12: 3.055, 13: 3.012, 14: 2.977, 15: 2.947, 16: 2.921, 17: 2.898, 18: 2.878, 19: 2.861, 20: 2.845, 21: 2.831, 22: 2.819, 23: 2.807, 24: 2.797, 25: 2.787, 26: 2.779, 27: 2.771, 28: 2.763, 29: 2.756, 30: 2.75, 32: 2.738, 34: 2.728, 36: 2.719, 38: 2.712, 40: 2.704, 42: 2.698, 44: 2.692, 46: 2.687, 48: 2.682, 50: 2.678, 55: 2.668, 60: 2.66, 65: 2.654, 70: 2.648, 80: 2.639, 100: 2.626, 150: 2.609, 200: 2.601};
function accept(tscore, freedom) {
var keys = Object.keys(table);
var key_low = keys.reduce(function(p, c) { if(freedom < c) return p; return c; });
var key_high = keys.reduce(function(p, c) { if(freedom > c) return p; return c; });
var span = key_high - key_low;
var critical = linear(table[key_low], table[key_high], (freedom - key_low) / span);
return tscore < critical;
}
function linear(a, b, t) { return a + (b - a) * t; }
}(self));