chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:07 +08:00
commit a26e856398
1681 changed files with 296950 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 * as fs from "node:fs";
import * as path from "node:path";
import * as all_benchmarks from "./cross_platform_suite.mjs";
import * as perspective_bench from "./src/js/benchmark.mjs";
import { createRequire } from "node:module";
import * as url from "node:url";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url)).slice(0, -1);
const _require = createRequire(import.meta.url);
/**
* We use the `dependencies` of this package for the benchmark candidate
* module list, so that we only need specify the dependencies and benchmark
* candidates in one place.
*/
const VERSIONS = Object.keys(
JSON.parse(fs.readFileSync(_require.resolve(`./package.json`)))
.dependencies,
);
fs.mkdirSync(path.join(__dirname, "./dist"), { recursive: true });
perspective_bench.suite(
// "ws://localhost:8082/websocket",
["@perspective-dev/client", ...VERSIONS],
path.join(__dirname, "dist/benchmark-js.arrow"),
async function (path, version_idx) {
let client, metadata;
if (path.startsWith("ws://")) {
console.log(path);
const { default: perspective } = await import(
"@perspective-dev/client"
);
client = await perspective.websocket(path);
metadata = {
version: "4.5.2",
version_idx,
};
} else {
const perspective = await import(path);
const pkg_json = JSON.parse(
fs.readFileSync(_require.resolve(`${path}/package.json`)),
);
let version = pkg_json.version;
console.log(`${path} (${pkg_json.name}@${version})`);
if (version === "@perspective-dev/client") {
version = `${version} (master)`;
}
client = perspective.default || perspective;
metadata = { version, version_idx };
}
globalThis.__SEND__ = (x) => process.send(x);
await all_benchmarks.table_suite(client, metadata);
await all_benchmarks.view_suite(client, metadata);
await all_benchmarks.to_data_suite(client, metadata);
await all_benchmarks.join_suite(client, metadata);
},
);
+111
View File
@@ -0,0 +1,111 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
export const CASES = [
{
name: "Y Line (split)",
config: {
plugin: "Y Line",
columns: ["Sales"],
// group_by: ["Category"],
split_by: ["Region"],
},
},
{
name: "Y Line",
config: {
plugin: "Y Line",
columns: ["Sales"],
// group_by: ["Category"],
},
},
{
name: "Y Bar",
config: {
plugin: "Y Bar",
columns: ["Sales"],
// group_by: ["Category"],
},
},
{
name: "X Bar",
config: {
plugin: "X Bar",
columns: ["Sales"],
// group_by: ["Category"],
},
},
{
name: "Y Area",
config: {
plugin: "Y Area",
columns: ["Sales"],
// group_by: ["Category"],
},
},
{
name: "Y Scatter",
config: {
plugin: "Y Scatter",
columns: ["Sales"],
// group_by: ["Category"],
},
},
{
name: "X/Y Scatter",
config: {
plugin: "X/Y Scatter",
columns: ["Quantity", "Profit"],
},
},
{
name: "Heatmap",
config: {
plugin: "Heatmap",
columns: ["Sales"],
group_by: ["Category"],
// split_by: ["Region"],
},
},
{
name: "Treemap",
config: {
plugin: "Treemap",
columns: ["Sales"],
// group_by: ["Category", "Sub-Category"],
},
},
// {
// name: "Sunburst",
// config: {
// plugin: "Sunburst",
// columns: ["Sales"],
// // group_by: ["State", "City"],
// },
// },
{
name: "Candlestick",
config: {
plugin: "Candlestick",
columns: ["Open", "Close", "High", "Low"],
// group_by: ["Category"],
},
},
{
name: "Y OHLC",
config: {
plugin: "OHLC",
columns: ["Open", "Close", "High", "Low"],
// group_by: ["Category"],
},
},
];
+59
View File
@@ -0,0 +1,59 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
// Installed into the page once via `page.addScriptTag`. Exposes:
// __BENCH_RESTORE__(config) - restore the viewer with `config`, drain RAFs,
// cache the active plugin and a fresh View.
// __BENCH_DRAW__() - drive one full plugin.draw(view) and return
// its wall-clock duration (ms).
//
// Important: `viewer.getView()` returns the *session-owned* View, not a clone.
// We never call `.delete()` on it — the viewer destroys it on the next
// `restore()`. See viewer.rs::getView docs.
(() => {
let _view = null;
let _plugin = null;
async function waitFrames(n) {
for (let i = 0; i < n; i++) {
await new Promise((r) => requestAnimationFrame(r));
}
}
window.__BENCH_RESTORE__ = async function (config) {
const viewer = document.querySelector("perspective-viewer");
await viewer.reset();
await viewer.restore(config);
// Drain the viewer's own scheduled render so the GL context is sized
// and warm before we start timing.
await viewer.flush();
await waitFrames(2);
_plugin = viewer.getPlugin();
_view = await viewer.getView();
};
window.__BENCH_DRAW__ = async function () {
if (!_plugin || !_view) {
throw new Error("__BENCH_DRAW__: call __BENCH_RESTORE__ first");
}
// `plugin.draw(view)` performs the full upload + GL submit. The
// returned promise resolves once `viewToColumnDataMap` has streamed
// every chunk through `_renderChunkData`. That is the unit we want.
const t0 = performance.now();
await _plugin.draw(_view);
const t1 = performance.now();
return t1 - t0;
};
})();
+135
View File
@@ -0,0 +1,135 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 * as fs from "node:fs";
import * as path from "node:path";
import * as url from "node:url";
import * as process from "node:process";
import { chromium } from "@playwright/test";
import * as perspective_bench from "./src/js/benchmark.mjs";
import { CASES } from "./charts_cases.mjs";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url)).slice(0, -1);
const PAGE_HARNESS = fs.readFileSync(
path.join(__dirname, "charts_page_harness.js"),
"utf8",
);
// Match the SwiftShader / DPR=1 launch flags from
// `tools/test/playwright.config.ts` so frame timings come from the
// deterministic software GL path, not whatever GPU the host has.
const LAUNCH_ARGS = [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
'--proxy-server="direct://"',
"--proxy-bypass-list=*",
"--js-flags=--expose-gc",
"--enable-precise-memory-info",
"--disable-background-timer-throttling",
"--disable-renderer-backgrounding",
"--disable-backgrounding-occluded-windows",
// "--use-gl=swiftshader",
// "--use-angle=swiftshader",
];
fs.mkdirSync(path.join(__dirname, "./dist"), { recursive: true });
// Single-version run — the workspace build. The `suite()` harness still
// expects an array; we only need one entry.
const VERSIONS = ["@perspective-dev/viewer-charts"];
perspective_bench.suite(
VERSIONS,
path.join(__dirname, "dist/benchmark-charts.arrow"),
async function run_charts_bench(_path, version_idx) {
const version = "workspace";
const browser = await chromium.launch({
headless: false,
args: LAUNCH_ARGS,
});
try {
const context = await browser.newContext({
viewport: { width: 1280, height: 720 },
deviceScaleFactor: 1,
});
const page = await context.newPage();
page.on("pageerror", (err) =>
console.error("page error:", err.message),
);
page.on("console", (msg) => {
if (msg.type() === "error") {
console.error("page console.error:", msg.text());
}
});
// The bench WebSocketServer (port 8081) mounts
// `["src/html/", "../.."]` relative to cwd. Our cwd is
// `tools/bench`, so `src/html/charts-bench.html` is served at
// `/charts-bench.html` and `/node_modules/...` resolves through
// the workspace root.
await page.goto("http://localhost:8081/charts-bench.html");
await page.waitForFunction(
() => window["__TEST_PERSPECTIVE_READY__"] === true,
null,
{ timeout: 60_000 },
);
await page.addScriptTag({ content: PAGE_HARNESS });
for (const c of CASES) {
// Run one `benchmark()` per case in the page context. The
// bench harness's browser branch uses `performance.now()`
// around our `test()`, and `__SEND__` collects observations
// we forward to the parent over IPC.
const items = await page.evaluate(
async ({ caseSpec, version, version_idx }) => {
const { benchmark } = await import(
"/tools/bench/src/js/benchmark.mjs"
);
const total = [];
window.__SEND__ = (x) => total.push(x);
await window.__BENCH_RESTORE__(caseSpec.config);
await benchmark({
name: `${caseSpec.name}`,
metadata: { version, version_idx },
async after() {
await new Promise((x) =>
requestAnimationFrame(x),
);
},
async test() {
await window.__BENCH_DRAW__();
},
});
return total;
},
{ caseSpec: c, version, version_idx },
);
for (const item of items) {
process.send(item);
}
// await page.pause();
}
} finally {
await browser.close();
}
},
);
+344
View File
@@ -0,0 +1,344 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 { benchmark } from "./src/js/benchmark.mjs";
import {
check_version_gte,
new_superstore_table,
} from "./src/js/superstore.mjs";
export async function join_suite(perspective, metadata) {
if (check_version_gte(metadata.version, "4.4.0")) {
async function before_all() {
const left = await perspective.table(
new_superstore_table(metadata),
);
const columns = await left.columns();
const expressions = Object.fromEntries(
columns
.filter((x) => x !== "Row ID")
.map((x) => [`${x}_2`, `"${x}"`]),
);
const view = await left.view({
columns: ["Row ID", ...Object.keys(expressions)],
expressions,
});
const right = await perspective.table(await view.to_arrow());
await view.delete();
return { left, right };
}
async function after_all({ left, right }) {
await left.delete();
await right.delete();
}
await benchmark({
name: `.join()`,
before_all,
after_all,
metadata,
async after(_, joined) {
await joined.delete();
},
async test({ left, right }) {
return await perspective.join(left, right, "Row ID");
},
});
}
}
export async function to_data_suite(perspective, metadata) {
async function before_all() {
const table = await perspective.table(new_superstore_table(metadata));
const view = await table.view();
return { table, view };
}
async function after_all({ table, view }) {
if (check_version_gte(metadata.version, "2.10.9")) {
await view.delete();
}
if (check_version_gte(metadata.version, "3.0.0")) {
await table.delete();
}
}
await benchmark({
name: `.to_arrow()`,
before_all,
after_all,
metadata,
async test({ view }) {
const _arrow = await view.to_arrow();
},
});
await benchmark({
name: `.to_csv()`,
before_all,
after_all,
metadata,
async test({ view }) {
const _csv = await view.to_csv();
},
});
await benchmark({
name: `.to_columns()`,
before_all,
after_all,
metadata,
async test({ view }) {
const _columns = await view.to_columns();
},
});
await benchmark({
name: `.to_json()`,
before_all,
after_all,
metadata,
async test({ view }) {
const _json = await view.to_json();
},
});
}
export async function view_suite(perspective, metadata) {
async function before_all() {
const table = await perspective.table(new_superstore_table(metadata));
const schema = await table.schema();
return { table, schema };
}
async function after_all({ table }) {
if (check_version_gte(metadata.version, "3.0.0")) {
await table.delete();
}
}
async function after({ table }, view) {
if (check_version_gte(metadata.version, "2.10.9")) {
await view.delete();
}
}
await benchmark({
name: `.view()`,
before_all,
after_all,
after,
metadata,
async test({ table }) {
return await table.view();
},
});
await benchmark({
name: `.view({group_by})`,
before_all,
after_all,
after,
metadata,
async test({ table }) {
if (check_version_gte(metadata.version, "1.2.0")) {
return await table.view({ group_by: ["Product Name"] });
} else {
return await table.view({ row_pivots: ["Product Name"] });
}
},
});
await benchmark({
name: `.view({expressions})`,
before_all,
after_all,
after,
metadata,
async test({ table }) {
if (check_version_gte(metadata.version, "2.7.0")) {
return await table.view({
columns: ["AAA"],
expressions: {
AAA: `("Sales" + "Profit") / 2`,
},
});
} else {
return await table.view({
columns: ["AAA"],
expressions: [`//AAA\n("Sales" + "Profit") / 2`],
});
}
},
});
await benchmark({
name: `.view({group_by, aggregates: "median"})`,
before_all,
after_all,
after,
metadata,
async test({ table, schema }) {
const columns = ["Sales", "Quantity", "City"];
const aggregates = Object.fromEntries(
Object.keys(schema).map((x) => [x, "median"]),
);
if (check_version_gte(metadata.version, "1.2.0")) {
return await table.view({
group_by: ["State"],
aggregates,
columns,
});
} else {
return await table.view({
row_pivots: ["State"],
aggregates,
columns,
});
}
},
});
}
export async function table_suite(perspective, metadata) {
async function before_all() {
try {
const table = await perspective.table(
new_superstore_table(metadata),
);
const view = await table.view();
const csv = await view.to_csv();
const arrow = await view.to_arrow();
const json = await view.to_json();
const columns = await view.to_columns();
if (check_version_gte(metadata.version, "2.10.9")) {
await view.delete();
}
if (check_version_gte(metadata.version, "3.0.0")) {
await table.delete();
}
return { csv, arrow, table, json, columns };
} catch (e) {
console.error(e);
}
}
if (check_version_gte(metadata.version, "2.3.0")) {
await benchmark({
name: `.table(arrow, {limit: 1000})`,
before_all,
metadata,
async after(_, table) {
if (check_version_gte(metadata.version, "3.0.0")) {
await table.delete();
}
},
async test({ arrow }) {
return await perspective.table(arrow.slice(), { limit: 1000 });
},
});
}
await benchmark({
name: `.table(arrow)`,
before_all,
metadata,
async after(_, table) {
if (check_version_gte(metadata.version, "3.0.0")) {
await table.delete();
}
},
async test({ table, arrow }) {
return await perspective.table(arrow.slice());
},
});
if (check_version_gte(metadata.version, "3.0.0")) {
await benchmark({
name: `table.update(arrow)`,
before_all,
metadata,
async before({ arrow }) {
let table2 = await perspective.table(arrow.slice(), {
limit: 1000,
});
return table2;
},
async after(_, table) {
if (!check_version_gte(metadata.version, "3.4.3")) {
// Bug with old versions of perspective segfault when you delete
// a table with pending updates.
await table.size();
}
await table.delete();
},
async test({ arrow }, table2) {
for (let i = 0; i < 3; i++) {
await table2.update(arrow.slice());
}
},
});
}
await benchmark({
name: `.table(csv)`,
before_all,
metadata,
async after(_, table) {
if (check_version_gte(metadata.version, "3.0.0")) {
await table.delete();
}
},
async test({ csv }) {
return await perspective.table(csv);
},
});
await benchmark({
name: `.table(json)`,
before_all,
metadata,
async after(_, table) {
if (check_version_gte(metadata.version, "3.0.0")) {
await table.delete();
}
},
async test({ table, json }) {
return await perspective.table(json);
},
});
await benchmark({
name: `.table(columns)`,
before_all,
metadata,
async after(_, table) {
if (check_version_gte(metadata.version, "3.0.0")) {
await table.delete();
}
},
async test({ table, columns }) {
return await perspective.table(columns);
},
});
}
+74
View File
@@ -0,0 +1,74 @@
{
"name": "@perspective-dev/bench",
"version": "2.0.1",
"description": "Benchmark utility based on perspective",
"private": true,
"main": "src/js/bench.mjs",
"files": [
"src/**/*",
"perspective-bench"
],
"bin": "perspective-bench",
"repository": {
"type": "git",
"url": "https://github.com/perspective-dev/perspective/packages/bench"
},
"scripts": {
"bench_js": "node --experimental-wasm-memory64 basic_suite.mjs",
"bench_python": "node --experimental-wasm-memory64 python_suite.mjs",
"bench_charts": "node --experimental-wasm-memory64 charts_suite.mjs"
},
"author": "",
"license": "Apache-2.0",
"devDependencies": {
"@perspective-dev/client": "workspace:",
"@perspective-dev/viewer-charts": "workspace:",
"@playwright/test": "catalog:",
"microtime": "catalog:",
"superstore-arrow": "catalog:",
"zx": "catalog:"
},
"dependencies": {
"perspective-4-5-1": "npm:@perspective-dev/client@4.5.1",
"perspective-4-5-0": "npm:@perspective-dev/client@4.5.0",
"perspective-4-4-0": "npm:@perspective-dev/client@4.4.0",
"perspective-4-3-0": "npm:@perspective-dev/client@4.3.0",
"perspective-4-2-0": "npm:@perspective-dev/client@4.2.0",
"perspective-4-1-0": "npm:@perspective-dev/client@4.1.0",
"perspective-4-0-0": "npm:@perspective-dev/client@4.0.0",
"perspective-3-8-0": "npm:@finos/perspective@3.8.0",
"perspective-3-7-0": "npm:@finos/perspective@3.7.0",
"perspective-3-6-0": "npm:@finos/perspective@3.6.0",
"perspective-3-5-0": "npm:@finos/perspective@3.5.0",
"perspective-3-4-0": "npm:@finos/perspective@3.4.0",
"perspective-3-3-0": "npm:@finos/perspective@3.3.0",
"perspective-3-2-0": "npm:@finos/perspective@3.2.0",
"perspective-3-1-0": "npm:@finos/perspective@3.1.0",
"perspective-3-0-0": "npm:@finos/perspective@3.0.0",
"perspective-2-10-0": "npm:@finos/perspective@2.10.0",
"perspective-2-9-0": "npm:@finos/perspective@2.9.0",
"perspective-2-8-0": "npm:@finos/perspective@2.8.0",
"perspective-2-7-0": "npm:@finos/perspective@2.7.0",
"perspective-2-6-0": "npm:@finos/perspective@2.6.0",
"perspective-2-5-0": "npm:@finos/perspective@2.5.0",
"perspective-2-4-0": "npm:@finos/perspective@2.4.0",
"perspective-2-3-0": "npm:@finos/perspective@2.3.0",
"perspective-2-2-0": "npm:@finos/perspective@2.2.0",
"perspective-2-1-0": "npm:@finos/perspective@2.1.0",
"perspective-1-1-0": "npm:@finos/perspective@1.1.0",
"perspective-1-0-0": "npm:@finos/perspective@1.0.0",
"perspective-0-10-0": "npm:@finos/perspective@0.10.0",
"perspective-0-9-0": "npm:@finos/perspective@0.9.0"
},
"deprecatedDependencies": {
"perspective-2-0-0": "npm:@finos/perspective@2.0.0",
"perspective-1-9-0": "npm:@finos/perspective@1.9.0",
"perspective-1-8-0": "npm:@finos/perspective@1.8.0",
"perspective-1-7-0": "npm:@finos/perspective@1.7.0",
"perspective-1-6-0": "npm:@finos/perspective@1.6.0",
"perspective-1-5-0": "npm:@finos/perspective@1.5.0",
"perspective-1-4-0": "npm:@finos/perspective@1.4.0",
"perspective-1-3-0": "npm:@finos/perspective@1.3.0",
"perspective-1-2-0": "npm:@finos/perspective@1.2.0"
}
}
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env node
const { registerCmd } = require("@perspective-dev/bench");
registerCmd();
+85
View File
@@ -0,0 +1,85 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 * as all_benchmarks from "./cross_platform_suite.mjs";
import * as perspective_bench from "./src/js/benchmark.mjs";
import * as puppeteer from "puppeteer";
import * as fs from "node:fs";
import * as path from "node:path";
import * as url from "node:url";
import * as process from "node:process";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url)).slice(0, -1);
/**
* We use the `dependencies` of this package for the benchmark candidate
* module list, so that we only need specify the dependencies and benchmark
* candidates in one place.
*/
const VERSIONS = [
"@perspective-dev/client",
"perspective-3-0-0",
"perspective-2-10-0",
];
perspective_bench.suite(
[...VERSIONS],
path.join(__dirname, "dist/benchmark-js.arrow"),
async function (path, version_idx) {
let client, metadata;
console.log(path);
const browser = await puppeteer.launch({
headless: true,
protocolTimeout: 100_000_000,
});
const page = await browser.newPage();
await page.goto("http://localhost:8081/empty.html");
async function test_suite(suite) {
const items = await page.evaluate(
async ([version, suite, version_idx]) => {
const { default: perspective } = await import(
`/tools/bench/node_modules/${version}/dist/esm/perspective.inline.js`
);
const benchmarks = await import(
"/tools/bench/cross_platform_suite.mjs"
);
const metadata = {
version,
version_idx,
};
const total = [];
window.__SEND__ = (x) => {
total.push(x);
};
const client = await perspective.worker();
await benchmarks[suite](client, metadata);
return total;
},
[path, suite, version_idx],
);
for (const { obs_records, stats } of items) {
process.send({ obs_records, stats });
}
}
await test_suite("table_suite");
await test_suite("view_suite");
await test_suite("to_data_suite");
await test_suite("join_suite");
},
);
+79
View File
@@ -0,0 +1,79 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 python_suite
*
* Run the Python benchmarks against hte Node.js client.
*/
import * as fs from "node:fs";
import * as path from "node:path";
import * as url from "node:url";
import * as python from "./src/js/servers/python.mjs";
import * as all_benchmarks from "./cross_platform_suite.mjs";
import * as perspective_bench from "./src/js/benchmark.mjs";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url)).slice(0, -1);
const CLIENT_VERSION = {
master: "@perspective-dev/client",
"4.5.1": "perspective-4-5-1",
"4.5.0": "perspective-4-5-0",
"4.4.0": "perspective-4-4-0",
"4.3.0": "perspective-4-3-0",
"4.2.0": "perspective-4-2-0",
"4.1.0": "perspective-4-1-0",
"4.0.0": "perspective-4-0-0",
"3.8.0": "perspective-3-8-0",
"3.6.0": "perspective-3-6-0",
"3.5.0": "perspective-3-5-0",
"3.4.0": "perspective-3-4-0",
"3.3.0": "perspective-3-3-0",
"3.2.0": "perspective-3-2-0",
"3.1.0": "perspective-3-1-0",
"3.0.3": "perspective-3-0-0",
"2.10.1": "perspective-2-10-0",
"2.9.0": "perspective-2-9-0",
"2.8.0": "perspective-2-8-0",
"2.7.0": "perspective-2-7-0",
"2.6.0": "perspective-2-6-0",
"2.5.0": "perspective-2-5-0",
"2.4.0": "perspective-2-4-0",
"2.3.2": "perspective-2-3-0",
"2.3.1": "perspective-2-3-0",
};
fs.mkdirSync(path.join(__dirname, "./dist"), { recursive: true });
perspective_bench.suite(
[...Object.keys(CLIENT_VERSION)],
path.join(__dirname, "dist/benchmark-python.arrow"),
async function (version, version_idx) {
console.log(version);
const { default: perspective } = await import(CLIENT_VERSION[version]);
const client = await perspective.websocket(
"ws://127.0.0.1:8082/websocket",
{ maxPayload: 1024 * 1024 * 1024 },
);
const metadata = { version, version_idx };
globalThis.__SEND__ = (x) => process.send(x);
await all_benchmarks.table_suite(client, metadata);
await all_benchmarks.view_suite(client, metadata);
await all_benchmarks.to_data_suite(client, metadata);
await all_benchmarks.join_suite(client, metadata);
},
python.start,
python.stop,
);
+80
View File
@@ -0,0 +1,80 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
const fs = require("fs");
import * as all_benchmarks from "./cross_platform_suite.mjs";
import * as perspective_bench from "./src/js/benchmark.mjs";
/**
* We use the `dependencies` of this package for the benchmark candidate
* module list, so that we only need specify the dependencies and benchmark
* candidates in one place.
*/
const VERSIONS = Object.keys(
JSON.parse(fs.readFileSync(require.resolve(`./package.json`))).dependencies,
);
perspective_bench.suite(
// ["@perspective-dev/client", ...VERSIONS],
[...VERSIONS],
async function (path, version_idx) {
let perspective = await import(path);
perspective = perspective.default || perspective;
const pkg_json = JSON.parse(
fs.readFileSync(require.resolve(`${path}/package.json`)),
);
let version = pkg_json.version;
console.log(`${path} (${pkg_json.name}@${version})`);
if (version_idx === 0) {
version = `${version} (master)`;
}
const metadata = { version, version_idx };
await all_benchmarks.table_suite(perspective, metadata);
// await all_benchmarks.view_suite(perspective, metadata);
// await all_benchmarks.to_data_suite(perspective, metadata);
},
async function (path) {
let psp = await import(path);
psp = psp.default || psp;
// const port = Math.floor(Math.random() * 1000) + 1000;
const server = new psp.WebSocketServer({
port: 8080,
});
function buffer_to_arraybuffer(buffer) {
return new Int8Array(
buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.length,
),
);
}
const table = await psp.table(
buffer_to_arraybuffer(
fs.readFileSync(
"../../node_modules/superstore-arrow/superstore.arrow",
),
).buffer,
{ name: "superstore" },
);
// // Legacy compat
if (server.host_table) {
server.host_table("superstore", table);
}
return server;
},
);
+113
View File
@@ -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>
View File
+38
View File
@@ -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>
+359
View File
@@ -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);
}
}
+86
View File
@@ -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) {}
});
+78
View File
@@ -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;
}
+101
View File
@@ -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()