chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" href="/node_modules/@fontsource/roboto-mono/400.css" />
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-family: "Roboto Mono" !important;
|
||||
--preload-fonts: "Roboto Mono:400";
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="module">
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
// Synthetic dataset for chart benchmarking. The columns are
|
||||
// chosen to exercise the same chart shapes as the real data
|
||||
// (group_by / split_by / two-numeric-axes), without depending
|
||||
// on superstore.csv parsing time.
|
||||
const ROW_COUNT = 10_000;
|
||||
const CATEGORIES = ["Furniture", "Office Supplies", "Technology", "Apparel", "Books"];
|
||||
|
||||
const REGIONS = ["North", "South", "East", "West", "Central"];
|
||||
const SUBCATS = ["Phones", "Chairs", "Tables", "Storage", "Binders", "Paper", "Art", "Accessories", "Copiers", "Machines"];
|
||||
|
||||
function build_data(n) {
|
||||
const Category = new Array(n);
|
||||
const Region = new Array(n);
|
||||
const SubCategory = new Array(n);
|
||||
const Sales = new Array(n);
|
||||
const Profit = new Array(n);
|
||||
const Quantity = new Array(n);
|
||||
const Discount = new Array(n);
|
||||
const Open = new Array(n);
|
||||
const High = new Array(n);
|
||||
const Low = new Array(n);
|
||||
const Close = new Array(n);
|
||||
|
||||
// Deterministic pseudo-random so iterations are comparable.
|
||||
let seed = 0x9e3779b9;
|
||||
const rand = () => {
|
||||
seed |= 0;
|
||||
seed = (seed + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
Category[i] = CATEGORIES[i % CATEGORIES.length];
|
||||
Region[i] = REGIONS[(i * 7) % REGIONS.length];
|
||||
SubCategory[i] = SUBCATS[(i * 13) % SUBCATS.length];
|
||||
const base = 100 + rand() * 900;
|
||||
Sales[i] = base;
|
||||
Profit[i] = (rand() - 0.4) * base * 0.3;
|
||||
Quantity[i] = 1 + Math.floor(rand() * 20);
|
||||
Discount[i] = Math.round(rand() * 100) / 100;
|
||||
const o = base;
|
||||
const c = base + (rand() - 0.5) * base * 0.05;
|
||||
Open[i] = o;
|
||||
Close[i] = c;
|
||||
High[i] = Math.max(o, c) + rand() * base * 0.02;
|
||||
Low[i] = Math.min(o, c) - rand() * base * 0.02;
|
||||
}
|
||||
|
||||
return {
|
||||
Category,
|
||||
Region,
|
||||
"Sub-Category": SubCategory,
|
||||
Sales,
|
||||
Profit,
|
||||
Quantity,
|
||||
Discount,
|
||||
Open,
|
||||
High,
|
||||
Low,
|
||||
Close,
|
||||
};
|
||||
}
|
||||
|
||||
await customElements.whenDefined("perspective-viewer");
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
const worker = await perspective.worker();
|
||||
const table = await worker.table(build_data(ROW_COUNT), {
|
||||
name: "charts-bench",
|
||||
});
|
||||
|
||||
await viewer.load(table);
|
||||
window.__TEST_WORKER__ = worker;
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<perspective-viewer></perspective-viewer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,38 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js"></script>
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/themes.css" />
|
||||
<style>
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<perspective-viewer></perspective-viewer>
|
||||
<script type="module">
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
const el = document.getElementsByTagName("perspective-viewer")[0];
|
||||
el.addEventListener("perspective-config-update", async () => {
|
||||
const config = await el.save();
|
||||
delete config["table"];
|
||||
localStorage.setItem("layout", JSON.stringify(config));
|
||||
});
|
||||
|
||||
const websocket = await perspective.websocket("ws://localhost:8081/subscribe");
|
||||
const table = await websocket.open_table("benchmarks");
|
||||
const worker = await perspective.worker();
|
||||
const table2 = await worker.table(await table.view());
|
||||
el.load(table2);
|
||||
el.restore(JSON.parse(localStorage.getItem("layout")));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,359 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
/**
|
||||
* @module perspective_bench
|
||||
* The main entrypoint of `perspective_bench` is a CLI node.js application which
|
||||
* may star
|
||||
*/
|
||||
|
||||
const MAX_ITERATIONS = 100;
|
||||
const MIN_ITERATIONS = 5;
|
||||
const WARM_UP_ITERATIONS = 10;
|
||||
|
||||
/**
|
||||
* Utility function to push/filter.
|
||||
* @param {*} a
|
||||
* @param {*} x
|
||||
*/
|
||||
function push_if(a, x) {
|
||||
if (x !== undefined) {
|
||||
a.push(x);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate avg fora key.
|
||||
* @param {*} a
|
||||
* @param {*} key
|
||||
* @returns
|
||||
*/
|
||||
function avg(a, key) {
|
||||
return a.map((x) => x[key]).reduce((x, y) => x + y, 0) / a.length / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate stddev for a key.
|
||||
* @param {*} array
|
||||
* @param {*} key
|
||||
* @returns
|
||||
*/
|
||||
function stddev(array, key) {
|
||||
const n = array.length;
|
||||
const mean = array.reduce((a, b) => a + b[key] / 1000, 0) / n;
|
||||
return Math.sqrt(
|
||||
array
|
||||
.map((x) => Math.pow(x[key] / 1000 - mean, 2))
|
||||
.reduce((a, b) => a + b) / n,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate outliers
|
||||
* @param {*} someArray
|
||||
* @returns
|
||||
*/
|
||||
function markOutliers(someArray) {
|
||||
var values = someArray.concat();
|
||||
values.sort(function (a, b) {
|
||||
return a.real_time - b.real_time;
|
||||
});
|
||||
|
||||
var q1 = values[Math.floor(values.length / 4)].real_time;
|
||||
var q3 = values[Math.ceil(values.length * (3 / 4))].real_time;
|
||||
var iqr = q3 - q1;
|
||||
var maxValue = q3 + iqr * 1.5;
|
||||
var minValue = q1 - iqr * 1.5;
|
||||
return someArray.map(function (x) {
|
||||
x.outlier = !(x.real_time <= maxValue && x.real_time >= minValue);
|
||||
return x;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a list to arrow and write it to disk. `@perspective-dev/client` is
|
||||
* imported in this scope to prevent interpreter-wide side effects of the
|
||||
* library from impacting forked processes, based on an observation that some
|
||||
* runs inline had anomalies across many observations that couldn't be explained
|
||||
* by contemporary system load.
|
||||
* @param {Array} obs_records an array of records to persist
|
||||
*/
|
||||
async function persist_to_arrow(benchmarks_table, out_path = "") {
|
||||
const fs = await import("node:fs");
|
||||
const view = await benchmarks_table.view();
|
||||
const arrow = await view.to_arrow();
|
||||
fs.writeFileSync(out_path, Buffer.from(arrow), "binary");
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single benchmark case, reporting the runtime
|
||||
* @param {*} param0
|
||||
* @returns An observation record with timing and metadata fields for a single
|
||||
* iteration of this case.
|
||||
*/
|
||||
async function benchmark_case({
|
||||
args,
|
||||
before,
|
||||
test,
|
||||
benchmark,
|
||||
metadata,
|
||||
after,
|
||||
i,
|
||||
}) {
|
||||
const args2 = args.slice();
|
||||
push_if(args2, await before?.(...args2));
|
||||
if (typeof process !== "undefined") {
|
||||
const { default: process } = await import("node:process");
|
||||
const { default: microtime } = await import("microtime");
|
||||
globalThis.gc(false);
|
||||
await new Promise((x) => setTimeout(x, 0));
|
||||
const start_time = microtime.now();
|
||||
const start_cpu = process.cpuUsage();
|
||||
const x = await test(...args2);
|
||||
const end = process.cpuUsage(start_cpu);
|
||||
const end_time = microtime.now() - start_time;
|
||||
push_if(args2, x);
|
||||
await after?.(...args2);
|
||||
return {
|
||||
cpu_time: end.user + end.system,
|
||||
real_time: end_time,
|
||||
user_time: end.user,
|
||||
system_time: end.system,
|
||||
benchmark,
|
||||
iteration: i,
|
||||
...structuredClone(metadata),
|
||||
};
|
||||
} else {
|
||||
const start_time = performance.now();
|
||||
const x = await test(...args2);
|
||||
const end_time = performance.now() - start_time;
|
||||
push_if(args2, x);
|
||||
await after?.(...args2);
|
||||
return {
|
||||
real_time: end_time * 1000,
|
||||
benchmark,
|
||||
iteration: i,
|
||||
...structuredClone(metadata),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the benchmarks in a forked process and colelct the observations.
|
||||
* @param {{version: string, i: number}} version the versions spec to send to
|
||||
* the child process.
|
||||
* @returns an array of observation records for this version.
|
||||
*/
|
||||
async function benchmark_node_version(version, benchmarks_table) {
|
||||
const cp = await import("node:child_process");
|
||||
const process = await import("node:process");
|
||||
const path = await import("node:path");
|
||||
const suite_path = path.join(process.argv[1]);
|
||||
let stats = [];
|
||||
const worker = cp.fork(suite_path, {
|
||||
execArgv: ["--expose-gc", "--experimental-wasm-memory64"],
|
||||
env: { BENCH_FLAG: "1" },
|
||||
});
|
||||
|
||||
let cont;
|
||||
worker.on("message", (details) => {
|
||||
if (details.finished) {
|
||||
cont();
|
||||
} else {
|
||||
let msg = " - ";
|
||||
if (!isNaN(details.stats.filtered_avg_cpu)) {
|
||||
msg += `${details.stats.filtered_avg_cpu}ms +/-${details.stats.stddev_percent}% (CPU), `;
|
||||
}
|
||||
msg += `${details.stats.filtered_avg_time}ms (Real) ${details.stats.non_outliers}/${details.stats.iterations} iterations - ${details.stats.benchmark}`;
|
||||
console.log(msg);
|
||||
benchmarks_table.update(details.obs_records);
|
||||
stats.push(details.stats);
|
||||
}
|
||||
});
|
||||
|
||||
worker.send({
|
||||
...version,
|
||||
stats,
|
||||
});
|
||||
|
||||
await new Promise((r) => {
|
||||
cont = r;
|
||||
});
|
||||
|
||||
worker.kill();
|
||||
return { stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a benchmark for a test case.
|
||||
* @param {*} param0
|
||||
*/
|
||||
export async function benchmark({
|
||||
name: benchmark,
|
||||
before,
|
||||
before_all,
|
||||
test,
|
||||
after,
|
||||
after_all,
|
||||
args = [],
|
||||
metadata = {},
|
||||
warm_up_iterations = WARM_UP_ITERATIONS,
|
||||
max_iterations = MAX_ITERATIONS,
|
||||
min_iterations = MIN_ITERATIONS,
|
||||
max_time = 3_000,
|
||||
} = {}) {
|
||||
let obs_records = [];
|
||||
push_if(args, await before_all?.(...args));
|
||||
const start_time = performance.now();
|
||||
for (let i = 0; i < warm_up_iterations; i++) {
|
||||
await benchmark_case({
|
||||
args,
|
||||
before,
|
||||
test,
|
||||
benchmark,
|
||||
metadata,
|
||||
after,
|
||||
i,
|
||||
});
|
||||
}
|
||||
|
||||
let i;
|
||||
for (i = 0; i < max_iterations; i++) {
|
||||
if (performance.now() - start_time > max_time && i >= min_iterations) {
|
||||
break;
|
||||
}
|
||||
|
||||
obs_records.push(
|
||||
await benchmark_case({
|
||||
args,
|
||||
before,
|
||||
test,
|
||||
benchmark,
|
||||
metadata,
|
||||
after,
|
||||
i,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
obs_records = markOutliers(obs_records);
|
||||
const filtered = obs_records.filter((x) => !x.outlier);
|
||||
const n_outliers = obs_records.length - filtered.length;
|
||||
const filtered_avg_cpu = avg(filtered, "cpu_time");
|
||||
const filtered_avg_time = avg(filtered, "real_time").toFixed(3);
|
||||
const stddev_cpu = stddev(filtered, "cpu_time").toFixed(3);
|
||||
const stats = {
|
||||
benchmark,
|
||||
metadata,
|
||||
filtered_avg_cpu: filtered_avg_cpu.toFixed(3),
|
||||
stddev_percent: ((stddev_cpu / filtered_avg_cpu) * 100).toFixed(1),
|
||||
stddev_cpu,
|
||||
filtered_avg_time,
|
||||
hr_time: avg(filtered, "hr_time"),
|
||||
iterations: i,
|
||||
non_outliers: i - n_outliers,
|
||||
};
|
||||
|
||||
await after_all?.(...args);
|
||||
globalThis.__SEND__({ obs_records, stats });
|
||||
}
|
||||
|
||||
function buffer_to_arraybuffer(buffer) {
|
||||
return new Int8Array(
|
||||
buffer.buffer.slice(
|
||||
buffer.byteOffset,
|
||||
buffer.byteOffset + buffer.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Host a Perspective server to view the live benchmark data as it accumulates.
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
async function start_server({
|
||||
cwd_static_file_handler,
|
||||
make_session,
|
||||
WebSocketServer,
|
||||
}) {
|
||||
return new WebSocketServer({ assets: ["src/html/", "../.."], port: 8081 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a suite of benchmarks to run against a set of packages with
|
||||
* similar APIs.
|
||||
* @param {*} versions
|
||||
* @param {*} run_version_callback
|
||||
*/
|
||||
export async function suite(
|
||||
versions,
|
||||
out_path,
|
||||
run_version_callback,
|
||||
start_server_callback,
|
||||
stop_server_callback,
|
||||
) {
|
||||
if (!!process.env.BENCH_FLAG) {
|
||||
const { default: process } = await import("node:process");
|
||||
process.on("message", async function bench_all({ path, i }) {
|
||||
await run_version_callback(path, i);
|
||||
process.send({ finished: true });
|
||||
});
|
||||
} else {
|
||||
const psp = await import("@perspective-dev/client");
|
||||
const benchmarks_table = await psp.default.table(
|
||||
{
|
||||
version: "string",
|
||||
cpu_time: "float",
|
||||
system_time: "float",
|
||||
user_time: "float",
|
||||
real_time: "float",
|
||||
version_idx: "integer",
|
||||
benchmark: "string",
|
||||
outlier: "boolean",
|
||||
},
|
||||
{ name: "benchmarks" },
|
||||
);
|
||||
|
||||
const app = await start_server(psp);
|
||||
|
||||
for (let i = 0; i < versions.length; i++) {
|
||||
let s;
|
||||
if (start_server_callback) {
|
||||
s = await start_server_callback(versions[i], i);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
benchmark_node_version(
|
||||
{ path: versions[i], i },
|
||||
benchmarks_table,
|
||||
),
|
||||
// benchmark_puppeteer_version(
|
||||
// { path: versions[i], i },
|
||||
// benchmarks_table
|
||||
// ),
|
||||
// benchmark_version({ path: versions[i], i }, benchmarks_table),
|
||||
// benchmark_version({ path: versions[i], i }, benchmarks_table),
|
||||
// benchmark_version({ path: versions[i], i }, benchmarks_table),
|
||||
]);
|
||||
|
||||
await persist_to_arrow(benchmarks_table, out_path);
|
||||
|
||||
if (stop_server_callback) {
|
||||
await stop_server_callback(s, versions[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
await app.close();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "zx/globals";
|
||||
|
||||
let SERVER;
|
||||
|
||||
/**
|
||||
* Create a Python `tornado` server, deleting and re-creating the virtual env
|
||||
* if necessary.
|
||||
*/
|
||||
export async function start(path) {
|
||||
const ac = new AbortController();
|
||||
let proc;
|
||||
await $`rm -rf benchmark_venv`;
|
||||
|
||||
if (path !== "master") {
|
||||
await $`python3 -m venv benchmark_venv`;
|
||||
const $$ = $({
|
||||
ac,
|
||||
prefix: "source benchmark_venv/bin/activate && ",
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
await $$`pip3 uninstall "perspective-python" -y`;
|
||||
await $$`pip3 install tornado "perspective-python==${path}"`;
|
||||
proc = $$`python3 src/python/server.py`;
|
||||
} else {
|
||||
proc = $`python3 src/python/server.py`;
|
||||
}
|
||||
|
||||
// Await server start
|
||||
let [resolve, sentinel] = (() => {
|
||||
let resolve;
|
||||
const sentinel = new Promise((x) => {
|
||||
resolve = x;
|
||||
});
|
||||
|
||||
return [resolve, sentinel];
|
||||
})();
|
||||
|
||||
async function await_log(pipe) {
|
||||
for await (const _ of proc[pipe]) {
|
||||
resolve?.();
|
||||
resolve = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
await_log("stdout");
|
||||
await_log("stderr");
|
||||
|
||||
await sentinel;
|
||||
SERVER = { ac, proc };
|
||||
return SERVER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a Python server.
|
||||
* @param {*} server
|
||||
*/
|
||||
export async function stop(server) {
|
||||
server.proc.kill();
|
||||
try {
|
||||
await server.proc;
|
||||
} catch (e) {}
|
||||
|
||||
await server.ac.abort();
|
||||
SERVER = undefined;
|
||||
}
|
||||
|
||||
process.on("SIGTERM", async () => {
|
||||
try {
|
||||
SERVER?.proc?.kill();
|
||||
await SERVER?.proc;
|
||||
await SERVER?.ac?.abort();
|
||||
} catch (e) {}
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
/**
|
||||
* Load a file as an `ArrayBuffer`, which is useful for loading Apache Arrow
|
||||
* Feather files.
|
||||
* @param {*} path
|
||||
* @returns
|
||||
*/
|
||||
async function get_buffer(path) {
|
||||
if (typeof window !== "undefined") {
|
||||
const resp = await fetch(
|
||||
"http://localhost:8080/node_modules/superstore-arrow/superstore.lz4.arrow",
|
||||
);
|
||||
|
||||
return await resp.arrayBuffer();
|
||||
} else {
|
||||
const fs = await import("node:fs");
|
||||
const { createRequire } = await import("node:module");
|
||||
const _require = createRequire(import.meta.url);
|
||||
return fs.readFileSync(_require.resolve(path)).buffer;
|
||||
}
|
||||
}
|
||||
|
||||
const SUPERSTORE_ARROW = await get_buffer("superstore-arrow/superstore.arrow");
|
||||
const SUPERSTORE_FEATHER = await get_buffer(
|
||||
"superstore-arrow/superstore.lz4.arrow",
|
||||
);
|
||||
|
||||
/**
|
||||
* Load the Superstore example data set as either a Feather (LZ4) or
|
||||
* uncompressed `Arrow`, depending on whether Perspective supports Feather.
|
||||
* @param {*} metadata
|
||||
* @returns
|
||||
*/
|
||||
export function new_superstore_table(metadata) {
|
||||
if (check_version_gte(metadata.version, "2.5.0")) {
|
||||
return SUPERSTORE_FEATHER.slice();
|
||||
} else {
|
||||
return SUPERSTORE_ARROW.slice();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a version string e.g. "v1.2.3" is greater or equal to another
|
||||
* version string, which must be of the same length/have the same number of
|
||||
* minor version levels.
|
||||
* @param {*} a
|
||||
* @param {*} b
|
||||
* @returns
|
||||
*/
|
||||
export function check_version_gte(a, b) {
|
||||
a = a.split(".").map((x) => parseInt(x));
|
||||
b = b.split(".").map((x) => parseInt(x));
|
||||
|
||||
if (a.length === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const i in a) {
|
||||
if (a[i] > b[i]) {
|
||||
return true;
|
||||
} else if (a[i] < b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
# ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
# This module handles historic versions of `perspective-python`'s API.
|
||||
|
||||
|
||||
import os
|
||||
import os.path
|
||||
|
||||
# import concurrent.futures
|
||||
import threading
|
||||
import tornado
|
||||
import perspective
|
||||
import perspective.handlers.tornado
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
file_path = os.path.join(
|
||||
here, "..", "..", "node_modules", "superstore-arrow", "superstore.lz4.arrow"
|
||||
)
|
||||
|
||||
if perspective.__version__.startswith("2"):
|
||||
from perspective import PerspectiveManager, PerspectiveTornadoHandler
|
||||
|
||||
def perspective_thread(
|
||||
manager,
|
||||
):
|
||||
psp_loop = tornado.ioloop.IOLoop()
|
||||
# [x, y, z] = map(int, perspective.__version__.split("."))
|
||||
# if x > 2 or (x > 1 and y > 2):
|
||||
# # with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
# # manager.set_loop_callback(psp_loop.run_in_executor, executor)
|
||||
# psp_loop.start()
|
||||
# else:
|
||||
# manager.set_loop_callback(psp_loop.add_callback)
|
||||
psp_loop.start()
|
||||
|
||||
def make_app():
|
||||
manager = PerspectiveManager()
|
||||
thread = threading.Thread(target=perspective_thread, args=(manager,))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
return tornado.web.Application(
|
||||
[
|
||||
(
|
||||
r"/websocket",
|
||||
PerspectiveTornadoHandler,
|
||||
{"manager": manager, "check_origin": True},
|
||||
),
|
||||
(
|
||||
r"/node_modules/(.*)",
|
||||
tornado.web.StaticFileHandler,
|
||||
{"path": "../../node_modules/"},
|
||||
),
|
||||
],
|
||||
websocket_max_message_size=1000 * 1024 * 1024,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = make_app()
|
||||
app.listen(8082)
|
||||
loop = tornado.ioloop.IOLoop.current()
|
||||
print("Listening on 8082", flush=True)
|
||||
loop.start()
|
||||
|
||||
else:
|
||||
|
||||
def make_app(perspective_server):
|
||||
return tornado.web.Application(
|
||||
[
|
||||
(
|
||||
r"/websocket",
|
||||
perspective.handlers.tornado.PerspectiveTornadoHandler,
|
||||
{"perspective_server": perspective_server},
|
||||
),
|
||||
(
|
||||
r"/node_modules/(.*)",
|
||||
tornado.web.StaticFileHandler,
|
||||
{"path": "../../node_modules/"},
|
||||
),
|
||||
],
|
||||
websocket_max_message_size=1000 * 1024 * 1024,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
perspective_server = perspective.Server()
|
||||
app = make_app(perspective_server)
|
||||
app.listen(8082)
|
||||
loop = tornado.ioloop.IOLoop.current()
|
||||
client = perspective_server.new_local_client()
|
||||
print("Listening on 8082", flush=True)
|
||||
loop.start()
|
||||
Reference in New Issue
Block a user