sparkforensics-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +147 -0
- package/lib/static-files.js +50 -0
- package/package.json +28 -0
- package/public/assets/AutoscalingChurn-CP5OjN6v.js +1 -0
- package/public/assets/CacheUtilization-BjyawXL6.js +1 -0
- package/public/assets/CachingOpportunity-DV2ABG2G.js +1 -0
- package/public/assets/ConfigAudit-DMiBhAoG.js +1 -0
- package/public/assets/CoreUsageArea-CGYIgPnt.js +1 -0
- package/public/assets/Dashboard-D1_IrWRs.js +12 -0
- package/public/assets/ExecutorTimeline-DkaAGqQT.js +1 -0
- package/public/assets/Failures-Crngoicu.js +1 -0
- package/public/assets/GcPressure-CFXm4BnA.js +1 -0
- package/public/assets/IncompleteRun-BWEJTb13.js +1 -0
- package/public/assets/JobFailures-BmjxCA-h.js +1 -0
- package/public/assets/MemoryUtilization-Bu2uQJbd.js +1 -0
- package/public/assets/PlanExplorer-DKUwF0uz.js +1 -0
- package/public/assets/PlanGraphRoute-BjPSugIG.js +1 -0
- package/public/assets/ShuffleIO--Rur3neu.js +1 -0
- package/public/assets/Spill-DIMpt8c2.js +1 -0
- package/public/assets/TaskSkew-BDr1DbeI.js +1 -0
- package/public/assets/charts-vendor-NMqKV0H4.js +59 -0
- package/public/assets/index-4id52G-A.js +9 -0
- package/public/assets/index-D9kwVYIO.css +1 -0
- package/public/assets/parser-worker-00O9WEQC.js +65 -0
- package/public/assets/plan-graph-vendor-BnuhLJ6X.css +1 -0
- package/public/assets/plan-graph-vendor-vZQHG2EJ.js +7 -0
- package/public/assets/react-vendor-u4pyvC5Y.js +9 -0
- package/public/assets/useRecentFiles-46iBU9zJ.js +5 -0
- package/public/docs/404.html +26 -0
- package/public/docs/assets/app.Bxy4_HVA.js +1 -0
- package/public/docs/assets/chunks/@localSearchIndexroot.DclgKtlT.js +1 -0
- package/public/docs/assets/chunks/VPLocalSearchBox.L5jNOIR3.js +9 -0
- package/public/docs/assets/chunks/framework.CNDaxUod.js +20 -0
- package/public/docs/assets/chunks/theme.CoT6y5bf.js +2 -0
- package/public/docs/assets/contributor-guide_architecture_board-widgets.md.BtjoE_1k.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_board-widgets.md.BtjoE_1k.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_detector-contract.md.DQyCHQNM.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_detector-contract.md.DQyCHQNM.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_drill-down.md.BKfyDQck.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_drill-down.md.BKfyDQck.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.CzlXOnCq.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.CzlXOnCq.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_index.md.94lNbF-4.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_index.md.94lNbF-4.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_overview.md.mQFlN66o.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_overview.md.mQFlN66o.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_state-and-history.md.DaqttKgQ.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_state-and-history.md.DaqttKgQ.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.Bk6Iyj9S.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.Bk6Iyj9S.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.BaS_L10g.js +6 -0
- package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.BaS_L10g.lean.js +1 -0
- package/public/docs/assets/contributor-guide_contributing.md.C-lKs70I.js +1 -0
- package/public/docs/assets/contributor-guide_contributing.md.C-lKs70I.lean.js +1 -0
- package/public/docs/assets/contributor-guide_development-setup.md.CpUMVtIK.js +12 -0
- package/public/docs/assets/contributor-guide_development-setup.md.CpUMVtIK.lean.js +1 -0
- package/public/docs/assets/contributor-guide_testing.md.C0q-BJfl.js +1 -0
- package/public/docs/assets/contributor-guide_testing.md.C0q-BJfl.lean.js +1 -0
- package/public/docs/assets/index.md.CETnDZzR.js +1 -0
- package/public/docs/assets/index.md.CETnDZzR.lean.js +1 -0
- package/public/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/public/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/public/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/public/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/public/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/public/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/public/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/public/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/public/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/public/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/public/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/public/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/public/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/public/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/public/docs/assets/style.D2mY0HUs.css +1 -0
- package/public/docs/assets/user-guide_getting-started.md.BJdD7a7_.js +3 -0
- package/public/docs/assets/user-guide_getting-started.md.BJdD7a7_.lean.js +1 -0
- package/public/docs/assets/user-guide_mcp-tools.md.DwYI1d6C.js +95 -0
- package/public/docs/assets/user-guide_mcp-tools.md.DwYI1d6C.lean.js +1 -0
- package/public/docs/assets/user-guide_run-comparison.md.5-oaOfTu.js +1 -0
- package/public/docs/assets/user-guide_run-comparison.md.5-oaOfTu.lean.js +1 -0
- package/public/docs/assets/user-guide_understanding-findings.md.CK2WZ0Tf.js +1 -0
- package/public/docs/assets/user-guide_understanding-findings.md.CK2WZ0Tf.lean.js +1 -0
- package/public/docs/contributor-guide/architecture/board-widgets.html +29 -0
- package/public/docs/contributor-guide/architecture/detector-contract.html +29 -0
- package/public/docs/contributor-guide/architecture/drill-down.html +29 -0
- package/public/docs/contributor-guide/architecture/impact-estimation.html +29 -0
- package/public/docs/contributor-guide/architecture/index.html +29 -0
- package/public/docs/contributor-guide/architecture/overview.html +29 -0
- package/public/docs/contributor-guide/architecture/state-and-history.html +29 -0
- package/public/docs/contributor-guide/architecture/widget-rendering.html +29 -0
- package/public/docs/contributor-guide/architecture/worker-protocol.html +34 -0
- package/public/docs/contributor-guide/contributing.html +29 -0
- package/public/docs/contributor-guide/development-setup.html +40 -0
- package/public/docs/contributor-guide/testing.html +29 -0
- package/public/docs/favicon.svg +4 -0
- package/public/docs/hashmap.json +1 -0
- package/public/docs/index.html +29 -0
- package/public/docs/user-guide/getting-started.html +31 -0
- package/public/docs/user-guide/mcp-tools.html +123 -0
- package/public/docs/user-guide/run-comparison.html +29 -0
- package/public/docs/user-guide/understanding-findings.html +29 -0
- package/public/docs/vp-icons.css +0 -0
- package/public/favicon.svg +4 -0
- package/public/index.html +36 -0
- package/public/vendor/spark-doc/anchors.json +205 -0
- package/public/vendor/spark-doc/index.html +4202 -0
- package/public/vendor/spark-doc/landing.html +418 -0
- package/public/vendor/spark-doc/meta.html +970 -0
- package/vendor-core/analyzer.js +167 -0
- package/vendor-core/assert-never.js +3 -0
- package/vendor-core/cli/budgets.js +203 -0
- package/vendor-core/cli/collect-run.js +107 -0
- package/vendor-core/core-count.js +68 -0
- package/vendor-core/core-locality-ratio.js +54 -0
- package/vendor-core/core-time-series.js +92 -0
- package/vendor-core/core-usage-locality.js +70 -0
- package/vendor-core/detectors.js +1989 -0
- package/vendor-core/docs-config.js +72 -0
- package/vendor-core/docs-site-config.js +23 -0
- package/vendor-core/efficiency-model.js +62 -0
- package/vendor-core/etl-phases.js +28 -0
- package/vendor-core/event-handlers.js +906 -0
- package/vendor-core/event-schemas.js +405 -0
- package/vendor-core/evidence-availability.js +121 -0
- package/vendor-core/evidence-report.js +459 -0
- package/vendor-core/finding-action-label.js +97 -0
- package/vendor-core/finding-filter-predicate.js +38 -0
- package/vendor-core/format-utils.js +167 -0
- package/vendor-core/impact-band.js +50 -0
- package/vendor-core/impact-estimator.js +428 -0
- package/vendor-core/ingest.js +139 -0
- package/vendor-core/job-groups.js +30 -0
- package/vendor-core/load-vendored.js +24 -0
- package/vendor-core/lz4-block.js +135 -0
- package/vendor-core/mcp-error.js +3 -0
- package/vendor-core/mcp-server-factory.js +115 -0
- package/vendor-core/mcp-tools.js +331 -0
- package/vendor-core/model-assembler.js +76 -0
- package/vendor-core/occupancy.js +202 -0
- package/vendor-core/parser-worker.js +249 -0
- package/vendor-core/plan-dot.js +25 -0
- package/vendor-core/plan-duration-attribution.js +185 -0
- package/vendor-core/plan-graph-model.js +171 -0
- package/vendor-core/plan-node-detail.js +159 -0
- package/vendor-core/plan-summary.js +233 -0
- package/vendor-core/plan-tree-walk.js +29 -0
- package/vendor-core/proxy.js +157 -0
- package/vendor-core/recommendation-rollup.js +197 -0
- package/vendor-core/redact.js +175 -0
- package/vendor-core/rolling-log-reassembly.js +52 -0
- package/vendor-core/run-aggregates.js +44 -0
- package/vendor-core/run-comparison.js +458 -0
- package/vendor-core/scaling-sim.js +73 -0
- package/vendor-core/session-snapshot.js +79 -0
- package/vendor-core/shs-fetch.js +196 -0
- package/vendor-core/shs-load.js +121 -0
- package/vendor-core/shs-request.js +101 -0
- package/vendor-core/shs-schemas.js +13 -0
- package/vendor-core/snappy-block.js +140 -0
- package/vendor-core/stage-quantiles.js +199 -0
- package/vendor-core/threshold-summary.js +35 -0
- package/vendor-core/types.js +286 -0
- package/vendor-core/vendor/fflate.js +2695 -0
- package/vendor-core/vendor/fzstd.js +768 -0
- package/vendor-core/wall-clock.js +36 -0
- package/vendor-core/wasted-core-hours.js +68 -0
package/index.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import { realpathSync, existsSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { resolveStaticRoot, serveStatic } from './lib/static-files.js';
|
|
7
|
+
// Unlike loadCreateMcpServer below, which falls back between a vendored copy
|
|
8
|
+
// and the repo-root packages/core/src/ sibling because *that file's location*
|
|
9
|
+
// varies by install mode, there's no equivalent fallback for the sdk package itself:
|
|
10
|
+
// mcp-server-factory imports it unconditionally too (vendored .js or repo
|
|
11
|
+
// .ts, either way), and packages/server/package.json
|
|
12
|
+
// (like the root package.json) declares '@modelcontextprotocol/sdk' as a direct
|
|
13
|
+
// dependency, so plain node module resolution finds it in both the published
|
|
14
|
+
// and monorepo-dev layouts.
|
|
15
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
16
|
+
|
|
17
|
+
const DEFAULT_PORT = 4173;
|
|
18
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
19
|
+
|
|
20
|
+
// Mirrors resolveStaticRoot's public/-vs-../dist fallback: packages/server/vendor-core/
|
|
21
|
+
// exists only in a published/standalone install (populated by
|
|
22
|
+
// scripts/vendor-core.mjs at publish time, which pre-strips the vendored core's
|
|
23
|
+
// TypeScript to plain .js since Node refuses to type-strip .ts under
|
|
24
|
+
// node_modules); in the monorepo it falls back to the real repo-root
|
|
25
|
+
// packages/core/src/ sibling directory. load-vendored.js itself is the one
|
|
26
|
+
// module this bootstrap has to locate by hand (see its own header comment);
|
|
27
|
+
// every other core module then loads through its exported loadVendored().
|
|
28
|
+
async function loadVendoredCore(dir, moduleName, opts) {
|
|
29
|
+
const vendoredHelper = join(dir, 'vendor-core', 'load-vendored.js');
|
|
30
|
+
const helperPath = existsSync(vendoredHelper) ? vendoredHelper : join(dir, '..', 'core', 'src', 'load-vendored.js');
|
|
31
|
+
const { loadVendored } = await import(pathToFileURL(helperPath).href);
|
|
32
|
+
return loadVendored(dir, moduleName, opts);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function loadCreateMcpServer(dir) {
|
|
36
|
+
const mod = await loadVendoredCore(dir, 'mcp-server-factory');
|
|
37
|
+
return mod.createMcpServer;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function loadHandleShsProxy(dir) {
|
|
41
|
+
const mod = await loadVendoredCore(dir, 'proxy', { srcExt: 'js' });
|
|
42
|
+
return mod.handleShsProxy;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createServer({
|
|
46
|
+
staticRoot,
|
|
47
|
+
fetchImpl = fetch,
|
|
48
|
+
// Test-only override for loadCreateMcpServer: lets test/index.test.js
|
|
49
|
+
// inject a promise that deliberately rejects, to prove a real failure
|
|
50
|
+
// still surfaces as a /mcp error response rather than being silently
|
|
51
|
+
// swallowed by the no-op .catch() below, without racing the real
|
|
52
|
+
// packages/server/vendor-core/ directory that test/bin.test.js
|
|
53
|
+
// separately populates and tears down.
|
|
54
|
+
loadCreateMcpServer: loadCreateMcpServerImpl = loadCreateMcpServer,
|
|
55
|
+
} = {}) {
|
|
56
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
57
|
+
const root = staticRoot ?? resolveStaticRoot(dir);
|
|
58
|
+
const createMcpServerPromise = loadCreateMcpServerImpl(dir);
|
|
59
|
+
const handleShsProxyPromise = loadHandleShsProxy(dir);
|
|
60
|
+
handleShsProxyPromise.catch(() => {});
|
|
61
|
+
// This promise is only ever awaited later, inside the /mcp branch below,
|
|
62
|
+
// so a request that never hits /mcp (most routing tests, and most real
|
|
63
|
+
// traffic to '/' or '/shs-proxy') leaves it completely unconsumed. If it
|
|
64
|
+
// rejects in that case (e.g. packages/server/vendor-core/mcp-server-factory.js
|
|
65
|
+
// transiently exists-but-is-mid-rewrite while test/bin.test.js's own real
|
|
66
|
+
// `npm pack` is running concurrently in another test file/worker), Node
|
|
67
|
+
// reports an unhandled rejection even though nothing actually needed the
|
|
68
|
+
// result yet. A promise's rejection is independently observable by every
|
|
69
|
+
// consumer that awaits/.catches it, so attaching this no-op handler here
|
|
70
|
+
// does not hide a real failure from the /mcp handler's own `await
|
|
71
|
+
// createMcpServerPromise` below; it only marks this one rejection as
|
|
72
|
+
// "handled" for Node's unhandledRejection bookkeeping.
|
|
73
|
+
createMcpServerPromise.catch(() => {});
|
|
74
|
+
|
|
75
|
+
return http.createServer(async (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const pathname = new URL(req.url, 'http://localhost').pathname;
|
|
78
|
+
if (pathname === '/shs-proxy') {
|
|
79
|
+
const handleShsProxy = await handleShsProxyPromise;
|
|
80
|
+
await handleShsProxy(req, res, { fetchImpl });
|
|
81
|
+
} else if (pathname === '/mcp') {
|
|
82
|
+
// Stateless mode (sessionIdGenerator: undefined) allows a transport to
|
|
83
|
+
// handle exactly one HTTP request; the SDK throws if it's reused, so a
|
|
84
|
+
// fresh server+transport pair is created per request (matches the SDK's
|
|
85
|
+
// own simpleStatelessStreamableHttp.ts example).
|
|
86
|
+
const createMcpServer = await createMcpServerPromise;
|
|
87
|
+
const mcpServer = createMcpServer();
|
|
88
|
+
// allowedHosts is matched against the raw Host header (host:port, no
|
|
89
|
+
// stripping) by the SDK, so the port must be derived from the actual
|
|
90
|
+
// listening socket rather than hardcoded: tests bind to an ephemeral
|
|
91
|
+
// port via listen(0, ...).
|
|
92
|
+
const localPort = req.socket.localPort;
|
|
93
|
+
const mcpTransport = new StreamableHTTPServerTransport({
|
|
94
|
+
sessionIdGenerator: undefined,
|
|
95
|
+
enableDnsRebindingProtection: true,
|
|
96
|
+
allowedHosts: [`127.0.0.1:${localPort}`, `localhost:${localPort}`],
|
|
97
|
+
});
|
|
98
|
+
await mcpServer.connect(mcpTransport);
|
|
99
|
+
// Each close() is isolated: a throw or rejection from one must not
|
|
100
|
+
// skip the other (leaking it) or become an unhandled rejection.
|
|
101
|
+
const closeQuietly = (target) => {
|
|
102
|
+
try { Promise.resolve(target.close()).catch(() => {}); } catch { /* already closed */ }
|
|
103
|
+
};
|
|
104
|
+
res.on('close', () => { closeQuietly(mcpTransport); closeQuietly(mcpServer); });
|
|
105
|
+
await mcpTransport.handleRequest(req, res);
|
|
106
|
+
} else {
|
|
107
|
+
await serveStatic(req, res, root);
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error(`[server] ${req.method} ${req.url} failed:`, err?.stack ?? err);
|
|
111
|
+
if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain' });
|
|
112
|
+
res.end(`Internal server error: ${err.message}`);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function startServer({ port = DEFAULT_PORT, host = DEFAULT_HOST, staticRoot } = {}) {
|
|
118
|
+
const server = createServer({ staticRoot });
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
server.listen(port, host, () => resolve(server));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function serverStartupGuidance(port) {
|
|
125
|
+
return [
|
|
126
|
+
`sparkforensics-server running at http://${DEFAULT_HOST}:${port}`,
|
|
127
|
+
'Open that URL in Chrome, then use the Fetch from Spark History Server disclosure.',
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parsePort(argv) {
|
|
132
|
+
const flagIdx = argv.indexOf('--port');
|
|
133
|
+
if (flagIdx !== -1 && argv[flagIdx + 1]) return Number(argv[flagIdx + 1]);
|
|
134
|
+
if (process.env.PORT) return Number(process.env.PORT);
|
|
135
|
+
return DEFAULT_PORT;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Run directly (node index.js / npx sparkforensics-server). argv[1] is
|
|
139
|
+
// the bin symlink path when launched via the published `.bin` entry, so it
|
|
140
|
+
// must be realpath-resolved before comparing against this module's real path.
|
|
141
|
+
const invokedDirectly = process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
142
|
+
if (invokedDirectly) {
|
|
143
|
+
const port = parsePort(process.argv);
|
|
144
|
+
startServer({ port }).then(() => {
|
|
145
|
+
for (const line of serverStartupGuidance(port)) console.log(line);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join, normalize, sep, extname } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const CONTENT_TYPES = {
|
|
6
|
+
'.html': 'text/html; charset=utf-8',
|
|
7
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
8
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
9
|
+
'.css': 'text/css; charset=utf-8',
|
|
10
|
+
'.json': 'application/json; charset=utf-8',
|
|
11
|
+
'.svg': 'image/svg+xml',
|
|
12
|
+
'.ico': 'image/x-icon',
|
|
13
|
+
'.map': 'application/json; charset=utf-8',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function resolveStaticRoot(dir) {
|
|
17
|
+
const bundled = join(dir, 'public');
|
|
18
|
+
return existsSync(bundled) ? bundled : join(dir, '..', 'dist');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function serveStatic(req, res, root) {
|
|
22
|
+
// Note: deliberately not routed through `new URL()`: its WHATWG dot-segment
|
|
23
|
+
// resolution collapses `/../../etc/passwd` to `/etc/passwd` before we ever see
|
|
24
|
+
// it, which would let a traversal attempt sail past the guard below as a
|
|
25
|
+
// harmless 404 instead of being caught and rejected with 403.
|
|
26
|
+
const pathname = decodeURIComponent(req.url.split('?')[0]);
|
|
27
|
+
const relative = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
|
|
28
|
+
const target = normalize(join(root, relative));
|
|
29
|
+
|
|
30
|
+
// Path-traversal guard: the resolved path must stay within root.
|
|
31
|
+
const rootPrefix = normalize(root + sep);
|
|
32
|
+
if (target !== normalize(root) && !target.startsWith(rootPrefix)) {
|
|
33
|
+
res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
|
|
34
|
+
res.end('Forbidden');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let data;
|
|
39
|
+
try {
|
|
40
|
+
data = await readFile(target);
|
|
41
|
+
} catch {
|
|
42
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
43
|
+
res.end('Not found');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const type = CONTENT_TYPES[extname(target).toLowerCase()] ?? 'application/octet-stream';
|
|
48
|
+
res.writeHead(200, { 'content-type': type });
|
|
49
|
+
res.end(data);
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sparkforensics-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"sparkforensics-server": "index.js"
|
|
7
|
+
},
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=18"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"lib/",
|
|
14
|
+
"public/",
|
|
15
|
+
"vendor-core/"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"prepack": "node ../../scripts/vendor-core.mjs . && node scripts/copy-frontend.js"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
23
|
+
"zod": "^4.4.3"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"vitest": "^4.1.10"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as C,j as e}from"./react-vendor-u4pyvC5Y.js";import{W as k,q as S,T as y,D as A,aw as T,a0 as w,az as N,I as R}from"./index-4id52G-A.js";import{d as I,C as L,a as g}from"./Dashboard-D1_IrWRs.js";import{i as K,C as $,X as z,Y as F,T as _,k as D,B as b,l as G}from"./charts-vendor-NMqKV0H4.js";import"./plan-graph-vendor-vZQHG2EJ.js";import"./useRecentFiles-46iBU9zJ.js";const H=60,O=6e4,Y=220;function U(t,s,a,c,r){if(t.length===0&&s.length===0)return[];const l=Math.max(r,c-a),x=Math.max(1,Math.ceil(l/r)),i=[];for(let n=0;n<x;n++)i.push({tStart:a+n*r,adds:0,removes:0});const u=n=>Math.min(x-1,Math.max(0,Math.floor((n-a)/r)));for(const n of t)i[u(n.timestamp)].adds++;for(const n of s)i[u(n.timestamp)].removes++;return i}function X(t,s){const a=Math.max(1,s-t);return Math.max(O,Math.ceil(a/H))}function q(t){if(t<=0)return 1;const a=10**Math.floor(Math.log10(t)),c=t/a;return(c<=1?1:c<=2?2:c<=5?5:10)*a}function P(t){const s=t.reduce((c,r)=>Math.max(c,Math.abs(r.added),Math.abs(r.removed)),0),a=q(s);return{domain:[-a,a],ticks:[-a,-a/2,0,a/2,a]}}function B(t){return Math.abs(t.height)<1?null:e.jsx(G,{...t})}function W({finding:t}){const{expanded:s,toggle:a}=T();return e.jsxs("div",{className:"flex flex-col gap-1 transition-colors",children:[e.jsxs("p",{className:"text-xs text-muted-foreground",children:["Short-lived executors: ",e.jsxs("strong",{children:[t.value,"%"]})]}),e.jsx(w,{finding:t}),e.jsx(N,{expanded:s,onClick:a,label:"autoscaling churn"}),s?e.jsxs("p",{className:"flex flex-wrap items-start gap-2 text-sm",children:[e.jsx(R,{impactBand:t.impactBand,className:"mt-1.5"}),e.jsx("span",{children:t.recommendation})]}):null]})}const se=C.memo(function({appModel:s,activeFileId:a,catalog:c=[],defaultCollapsed:r=!0}){const l=C.useMemo(()=>{const o=s.executors.added,m=s.executors.removed;if(o.length===0&&m.length===0)return{hasEvents:!1,added:o,removed:m};const f=s.app,h=f?.startTime??0,v=f?.endTime??h,E=X(h,v),j=I(U(o,m,h,v,E)).map(p=>({t:`${Math.round((p.tStart-h)/1e3)}s`,added:p.adds,removed:-p.removes})),M=P(j);return{hasEvents:!0,added:o,removed:m,rows:j,yAxisScale:M}},[s,a]);if(!l.hasEvents)return e.jsx(k,{title:"Autoscaling Churn",summary:e.jsx("span",{className:"text-muted-foreground text-xs",children:"No executor add/remove events (static allocation)."}),children:e.jsx("p",{className:"text-muted-foreground text-xs",children:"No executor add/remove events (static allocation)."})});const{added:x,removed:i,rows:u,yAxisScale:n}=l,d=c.find(o=>o.type==="autoscalingChurn");return e.jsx(k,{title:"Autoscaling Churn",impactBand:d?.impactBand,badges:e.jsxs(e.Fragment,{children:[d&&e.jsx(y,{type:"autoscalingChurn",impactBand:d.impactBand}),e.jsx(A,{anchor:"#config-autoscale-bounds",children:"Learn more →"})]}),defaultCollapsed:r,summary:e.jsx(S,{value:`${x.length} added / ${i.length} removed`,context:d?`${d.value}% short-lived executors — ${d.impactBand}`:"executor churn events"}),children:e.jsxs("div",{className:"space-y-2",children:[d&&e.jsx(W,{finding:d}),e.jsx("p",{className:"text-muted-foreground text-xs",children:"executors added / removed over time"}),e.jsx(L,{ariaLabel:"Executor added and removed counts per time bucket over the run.",height:Y,table:{caption:"Executor added/removed counts by time bucket",columns:["Time","Executors added","Executors removed"],rows:u.map(o=>[o.t,o.added,Math.abs(o.removed)]),align:["right","right","right"]},children:e.jsxs(K,{data:u,children:[e.jsx($,{stroke:g.muted,strokeOpacity:.2}),e.jsx(z,{dataKey:"t",tick:{fontSize:10}}),e.jsx(F,{tick:{fontSize:10},width:32,domain:n.domain,ticks:n.ticks}),e.jsx(_,{formatter:(o,m)=>[Math.abs(Number(o)),m]}),e.jsx(D,{wrapperStyle:{fontSize:10}}),e.jsx(b,{dataKey:"added",name:"Added",stackId:"churn",fill:g.clean,shape:B}),e.jsx(b,{dataKey:"removed",name:"Removed",stackId:"churn",fill:g.muted,shape:B})]})})]})})});export{se as AutoscalingChurn,U as bucketChurn};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as l,j as s}from"./react-vendor-u4pyvC5Y.js";import{K as y,at as v,as as k,W as z,j as M,l as F,m as P,n as d,o as L,p as m,T as E,J as b,Z as f,R as w,q as A,D as U,U as _,ax as $,ay as H,aw as W,I as O,a0 as q,az as G,aA as J}from"./index-4id52G-A.js";import"./charts-vendor-NMqKV0H4.js";import"./plan-graph-vendor-vZQHG2EJ.js";const S=70;function K(a){if(!a)return"—";const t=[];return a.useMemory&&t.push("Memory"),a.useDisk&&t.push("Disk"),a.deserialized?t.push("Deserialized"):(a.useMemory||a.useDisk)&&t.push("Serialized"),(a.replication??1)>1&&t.push(`x${a.replication}`),t.join(" · ")||"—"}function T(a){return a.name||`RDD ${a.id}`}function X(a){const t=_(T(a));return t.length<=S?t:`${t.slice(0,S)}…`}function Z({finding:a}){const t=$([a]),n=H([a]),{expanded:h,toggle:o}=W(),x=a.rddName??`RDD ${a.rddId}`;return s.jsxs("div",{ref:t,tabIndex:-1,"data-flashed":n?"true":"false",className:`space-y-1 transition-colors ${J(n)}`,children:[s.jsxs("p",{className:"flex flex-wrap items-start gap-2",children:[s.jsx(O,{impactBand:a.impactBand,className:"mt-1.5"}),s.jsx("span",{children:x})]}),s.jsx(q,{finding:a}),s.jsx(G,{expanded:h,onClick:o,label:x}),h?s.jsx("p",{className:"text-sm",children:a.recommendation}):null]})}const se=l.memo(function({appModel:t,catalog:n}){const[h,o]=l.useState(0),[x,u]=l.useState(0),g=t.app?.rddInfo,r=l.useMemo(()=>g?[...g.values()].filter(e=>(e.storageLevel?.useMemory||e.storageLevel?.useDisk)&&(e.numCachedPartitions??0)>0).sort((e,i)=>(i.memorySize??0)+(i.diskSize??0)-((e.memorySize??0)+(e.diskSize??0))):[],[g]),c=l.useMemo(()=>n.filter(e=>e.type==="cacheUtilization").sort((e,i)=>y[e.impactBand]-y[i.impactBand]),[n]),p=v(r,h,o),N=k(),B=N?c.findIndex(e=>e===N.finding):null,j=v(c,x,u,B);if(r.length===0)return s.jsx(z,{title:"Cache Utilization",summary:s.jsx("span",{className:"text-xs text-muted-foreground",children:"No cached RDDs in this run."}),children:s.jsx("p",{className:"text-xs text-muted-foreground",children:"No cached RDDs in this run."})});const C=r.reduce((e,i)=>e+(i.memorySize??0)+(i.diskSize??0),0),I=e=>c.filter(i=>i.rddId===e);return s.jsx("div",{"data-widget":"cache-utilization",children:s.jsxs(z,{title:"Cache Utilization",impactBand:b(c),badges:s.jsxs(s.Fragment,{children:[s.jsxs("span",{className:"text-xs text-muted-foreground",children:[r.length," cached RDD",r.length===1?"":"s"]}),s.jsx(U,{anchor:"#memory-model",children:"Learn more →"})]}),defaultCollapsed:!0,summary:s.jsx(A,{value:f(C),context:"cached across memory + disk"}),children:[s.jsxs(M,{children:[s.jsx(F,{children:s.jsxs(P,{children:[s.jsx(d,{children:"RDD"}),s.jsx(d,{children:"Storage level"}),s.jsx(d,{className:"text-right",children:"Cached partitions"}),s.jsx(d,{className:"text-right",children:"Memory"}),s.jsx(d,{className:"text-right",children:"Disk"})]})}),s.jsx(L,{children:p.visible.map(e=>{const i=T(e),D=X(e),R=I(e.id);return s.jsxs(P,{children:[s.jsxs(m,{className:"max-w-80 truncate",title:i!==D?i:void 0,children:[D,R.length>0&&s.jsx(E,{type:"cacheUtilization",impactBand:b(R)??"info",className:"ml-1.5"})]}),s.jsx(m,{children:K(e.storageLevel)}),s.jsxs(m,{className:"text-right",children:[e.numCachedPartitions," / ",e.numPartitions]}),s.jsx(m,{className:"text-right",children:f(e.memorySize??0)}),s.jsx(m,{className:"text-right",children:f(e.diskSize??0)})]},e.id)})})]}),s.jsx(w,{page:p.effectivePage,totalPages:p.totalPages,onPrev:()=>o(e=>e-1),onNext:()=>o(e=>e+1)}),c.length>0?s.jsxs("div",{className:"mt-3 space-y-2 text-sm",children:[j.visible.map(e=>s.jsx(Z,{finding:e},e.id)),s.jsx(w,{page:j.effectivePage,totalPages:j.totalPages,onPrev:()=>u(e=>e-1),onNext:()=>u(e=>e+1)})]}):null]})})});export{se as CacheUtilization};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as u,j as e}from"./react-vendor-u4pyvC5Y.js";import{c as v,as as b,at as N,W as f,j as w,l as C,m as h,n as c,o as T,R as B,q as k,T as $,ax as I,ay as P,aw as q,p as d,x as m,Z as E,a0 as O,az as F,aG as M,aA as W}from"./index-4id52G-A.js";import"./charts-vendor-NMqKV0H4.js";import"./plan-graph-vendor-vZQHG2EJ.js";const A=[["circle",{cx:"18",cy:"18",r:"3",key:"1xkwt0"}],["circle",{cx:"6",cy:"6",r:"3",key:"1lh9wr"}],["path",{d:"M6 21V9a9 9 0 0 0 9 9",key:"7kw0sc"}]],z=v("git-merge",A);function G({relations:a,operator:o}){const l=o==="union"?"union":"join";return e.jsx("span",{className:"inline-flex flex-wrap items-baseline",children:a.map((r,n)=>e.jsxs("span",{children:[n>0?e.jsxs(e.Fragment,{children:[" ",e.jsx("span",{className:"mx-1 font-semibold uppercase text-xs tracking-wide text-primary",children:l})," "]}):null,r.relation]},r.relation))})}function H({finding:a}){const o=I([a]),l=P([a]),{expanded:r,toggle:n}=q(),t=a.variant==="composite";return e.jsxs(h,{ref:o,tabIndex:-1,"data-flashed":l?"true":"false",className:`transition-colors ${W(l)}`,children:[e.jsx(d,{children:e.jsxs("span",{className:"inline-flex items-center gap-2",children:[t?e.jsx(z,{className:"size-3.5 text-muted-foreground","aria-hidden":"true"}):null,t&&a.relations&&a.relations.length>0?e.jsx(G,{relations:a.relations,operator:a.operator}):e.jsx("span",{children:a.relation}),t?e.jsx(m,{variant:"outline",className:"text-xs uppercase border-dashed",children:a.operator?.toUpperCase()}):e.jsx(m,{variant:"secondary",className:"text-xs uppercase",children:a.format})]})}),e.jsxs(d,{children:[a.value," queries"]}),e.jsx(d,{className:"text-right",children:E(a.totalReadBytes??0)}),e.jsxs(d,{children:[e.jsx(O,{finding:a}),e.jsx(F,{expanded:r,onClick:n,label:a.relation??"this relation"}),r?e.jsxs("div",{className:"mt-1 space-y-1",children:[e.jsx("p",{children:a.recommendation}),t?e.jsx(M,{confidence:a.confidence,validationRequired:a.validationRequired}):null]}):null]})]})}const U=u.memo(function({catalog:o,defaultCollapsed:l=!0}){const[r,n]=u.useState(0),t=u.useMemo(()=>o.filter(s=>s.type==="cachingOpportunity").sort((s,i)=>(i.totalReadBytes??0)-(s.totalReadBytes??0)||(typeof i.value=="number"?i.value:0)-(typeof s.value=="number"?s.value:0)),[o]),p=b(),j=p?t.findIndex(s=>s===p.finding):null,{totalPages:y,effectivePage:g,visible:R}=N(t,r,n,j);if(t.length===0)return null;const x=t[0];return e.jsx(f,{title:"Caching Opportunities",impactBand:"info",badges:e.jsx($,{type:"cachingOpportunity",impactBand:"info"}),defaultCollapsed:l,summary:e.jsx(k,{value:`${x.value}×`,context:`read by ${x.value} queries: ${x.relation}`}),children:e.jsxs("div",{className:"space-y-3",children:[e.jsxs(w,{children:[e.jsx(C,{children:e.jsxs(h,{children:[e.jsx(c,{children:"Relation"}),e.jsx(c,{children:"Read by"}),e.jsx(c,{className:"text-right",children:"Data read"}),e.jsx(c,{children:"Recommendation"})]})}),e.jsx(T,{children:R.map((s,i)=>e.jsx(H,{finding:s},`${s.format}:${s.relation}:${(s.executionIds??[]).join(",")}:${i}`))})]}),e.jsx(B,{page:g,totalPages:y,onPrev:()=>n(s=>s-1),onNext:()=>n(s=>s+1)})]})})});export{U as CachingOpportunity};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as p,j as e}from"./react-vendor-u4pyvC5Y.js";import{ae as N,K as h,as as b,at as k,E as d,W as f,J as A,R,q as I,T as B,ax as w,ay as E,aw as P,I as F,a0 as T,az as D,D as K,aA as S}from"./index-4id52G-A.js";import"./charts-vendor-NMqKV0H4.js";import"./plan-graph-vendor-vZQHG2EJ.js";function L({finding:a}){const t=w([a]),i=E([a]),{expanded:r,toggle:l}=P();return e.jsxs("li",{ref:t,tabIndex:-1,"data-flashed":i?"true":"false",className:`space-y-1 transition-colors ${S(i)}`,children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(F,{impactBand:a.impactBand}),e.jsx("code",{className:"text-xs",children:a.property})]}),e.jsx(T,{finding:a}),e.jsx(D,{expanded:r,onClick:l,label:a.property??"this property"}),r?e.jsxs("div",{className:"space-y-1",children:[e.jsxs("p",{className:"text-sm",children:[a.recommendation," ",e.jsx(d,{evidenceKey:"sparkConfiguration"}),a.docAnchor?e.jsxs(e.Fragment,{children:[" ",e.jsx(K,{anchor:a.docAnchor,children:"Learn more →"})]}):null]}),e.jsx("div",{className:"rounded-md border border-accent/30 bg-accent/10 px-3 py-2 text-xs",children:e.jsxs("code",{className:"rounded bg-background px-1.5 py-0.5 font-mono text-accent",children:[a.property," = ",String(a.value)]})})]}):null]})}const z=p.memo(function({appModel:t,configFindings:i,defaultCollapsed:r=!0}){const[l,m]=p.useState(0),o=p.useMemo(()=>(i??N(t.app)).filter(s=>s.type==="configAudit").slice().sort((s,n)=>h[s.impactBand]-h[n.impactBand]),[i,t]),u=b(),j=u?o.findIndex(s=>s===u.finding):null,{totalPages:y,effectivePage:v,visible:C}=k(o,l,m,j);if(o.length===0){const s=t.evidenceAvailability?.entries.find(c=>c.key==="sparkConfiguration");let n;if(s)s.state==="present"?n="No misconfigurations detected.":s.state==="unknown"&&s.reasonCode==="parseIncomplete"?n=e.jsxs(e.Fragment,{children:[e.jsxs("span",{children:[s.summary," "]}),e.jsx(d,{evidenceKey:"sparkConfiguration"})]}):n=e.jsxs(e.Fragment,{children:[e.jsx("span",{children:"No environment info captured in this log. "}),e.jsx(d,{evidenceKey:"sparkConfiguration"})]});else{const c=t.app?.evidenceInputs;n=(c?c.environmentUpdates>0:Object.keys(t.app?.config??{}).length>0||!!t.app?.resources)?"No misconfigurations detected.":e.jsxs(e.Fragment,{children:[e.jsx("span",{children:"No environment info captured in this log. "}),e.jsx(d,{evidenceKey:"sparkConfiguration"})]})}const g=e.jsx("p",{className:"text-muted-foreground text-xs",children:n});return e.jsx(f,{title:"Config Sanity",compact:!0,summary:g,children:g})}const x=A(o);return e.jsxs(f,{title:"Config Sanity",impactBand:x,badges:e.jsx(B,{type:"configAudit",impactBand:x??"info"}),defaultCollapsed:r,summary:e.jsx(I,{value:`${o.length}`,context:`misconfiguration${o.length===1?"":"s"} flagged`}),children:[e.jsx("ul",{className:"space-y-4",children:C.map(s=>e.jsx(L,{finding:s},s.id))}),e.jsx(R,{page:v,totalPages:y,onPrev:()=>m(s=>s-1),onNext:()=>m(s=>s+1)})]})});export{z as ConfigAudit};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as T,j as a}from"./react-vendor-u4pyvC5Y.js";import{aH as Y,at as G,W as M,I as X,a0 as q,R as $,q as J,T as Q,D as V}from"./index-4id52G-A.js";import{d as W,C as Z,a as l}from"./Dashboard-D1_IrWRs.js";import{j as tt,C as et,X as at,Y as st,T as ot,k as nt,A as it}from"./charts-vendor-NMqKV0H4.js";import"./plan-graph-vendor-vZQHG2EJ.js";import"./useRecentFiles-46iBU9zJ.js";const F=["PROCESS_LOCAL","NODE_LOCAL","RACK_LOCAL","NO_PREF","ANY"];function ct(I,{bucketWidthMs:n=6e4,tiers:y=F}={}){const m=I.filter(e=>(e.completedAt??0)>(e.submittedAt??0)&&(e.executorRunTime??0)>0);if(m.length===0)return{labels:[],series:{}};const f=m.reduce((e,s)=>Math.min(e,s.submittedAt??e),1/0),R=m.reduce((e,s)=>Math.max(e,s.completedAt??e),-1/0),r=Math.max(1,Math.ceil((R-f)/n)),i={};for(const e of y)i[e]=new Array(r).fill(0);const g=new Array(r).fill(0);for(const e of m){const s=e.completedAt??0,h=e.submittedAt??0,u=s-h,E=(e.executorRunTime??0)/u,S=(e.localityStats??[]).reduce((t,d)=>t+d.count,0)||1,b=new Map((e.localityStats??[]).map(t=>[t.locality,t.count/S]));for(let t=0;t<r;t++){const d=f+t*n,L=d+n,p=Math.min(s,L)-Math.max(h,d);if(p<=0)continue;const N=p/n;for(const[C,k]of b){const x=E*k*N;i[C]?i[C][t]+=x:g[t]+=x}}}g.some(e=>e>0)&&(i.OTHER=g);const o=new Array(r).fill(0);for(const e of Object.keys(i))for(let s=0;s<r;s++)o[s]+=i[e][s];const v=o.reduce((e,s)=>Math.max(e,s),0);i.idle=o.map(e=>Math.max(0,v-e));const A=[];for(let e=0;e<r;e++)A.push(f+e*n);return{labels:A,series:i}}const rt=240,lt=60,B={PROCESS_LOCAL:"Process-local",NODE_LOCAL:"Node-local",RACK_LOCAL:"Rack-local",NO_PREF:"No preference",ANY:"Any",OTHER:"Other",idle:"Idle"},H={PROCESS_LOCAL:l.clean,NODE_LOCAL:l.accent,RACK_LOCAL:l.warning,NO_PREF:l.muted,ANY:l.critical,OTHER:l.muted,idle:l.muted},z="Core Usage by Locality",gt=T.memo(function({appModel:n,catalog:y,activeFileId:m,defaultCollapsed:f=!0}){const[R,r]=T.useState(0),i=T.useMemo(()=>{const t=[...n.stages.values()];if(!t.some(c=>(c.executorRunTime??0)>0&&(c.completedAt??0)>(c.submittedAt??0)))return{hasActivity:!1};const L=n.app,p=L?.startTime??0,N=L?.endTime??p,C=Math.max(6e4,Math.ceil(Math.max(1,N-p)/lt)),{labels:k,series:x}=ct(t,{bucketWidthMs:C}),P=[...F.filter(c=>x[c]),...x.OTHER?["OTHER"]:[],"idle"],_=k.map((c,w)=>{const j={t:Math.round((c-p)/1e3)};for(const O of P)j[O]=x[O]?.[w]??0;return j}),D=W(_),K=P.filter(c=>c!=="idle"),U=_.reduce((c,w)=>Math.max(c,K.reduce((j,O)=>j+w[O],0)),0);return{hasActivity:!0,order:P,points:_,sampled:D,peakCores:U}},[n,m]),g=T.useMemo(()=>Y([...n.stages.values()],{topN:1/0}),[n,m]),o=y.find(t=>t.type==="coreLocality"),v=y.some(t=>t.type==="memoryUtilization"&&t.variant==="idleCores"),A=g.topStages.filter(t=>t.nonLocalTasks>0),{totalPages:e,effectivePage:s,visible:h}=G(A,R,r);if(!i.hasActivity)return a.jsx(M,{title:z,summary:a.jsx("span",{className:"text-muted-foreground text-xs",children:"No stage activity to plot."}),children:a.jsx("p",{className:"text-muted-foreground text-xs",children:"No stage activity to plot."})});const{order:u,points:E,sampled:S,peakCores:b}=i;return a.jsx(M,{title:z,impactBand:o?.impactBand,badges:a.jsxs(a.Fragment,{children:[o?a.jsx(Q,{type:"coreLocality",impactBand:o.impactBand}):null,a.jsx(V,{anchor:"#bottleneck-utilization",children:"Learn more →"})]}),defaultCollapsed:f,summary:a.jsx(J,{value:`${Math.round(b)} cores`,context:"peak concurrent, by locality"}),children:a.jsxs("div",{className:"space-y-2",children:[a.jsx("p",{className:"text-muted-foreground text-xs",children:"approximate: stage-level attribution"}),o?a.jsxs("p",{className:"flex flex-wrap items-start gap-2 text-sm",children:[a.jsx(X,{impactBand:o.impactBand,className:"mt-1.5"}),a.jsxs("span",{children:[a.jsxs("strong",{children:[o.value,"% non-local"]}),": ",o.recommendation]})]}):null,o?a.jsx(q,{finding:o}):null,a.jsx(Z,{ariaLabel:"Approximate concurrent core usage over time, stacked by task locality tier, with idle capacity shown. See the legend for tier colors.",height:rt,table:{caption:"Concurrent core usage over time by locality tier",columns:["Time (s)",...u.map(t=>B[t]??t)],rows:E.map(t=>[t.t,...u.map(d=>Math.round(t[d]))]),align:Array.from({length:u.length+1},()=>"right")},children:a.jsxs(tt,{data:S,children:[a.jsx(et,{vertical:!1,stroke:l.muted,strokeOpacity:.2}),a.jsx(at,{dataKey:"t",tick:{fontSize:10},label:{value:"seconds from app start",position:"insideBottom",offset:-2,fontSize:10}}),a.jsx(st,{tick:{fontSize:10},width:36,label:{value:"avg concurrent cores",angle:-90,position:"insideLeft",fontSize:10}}),a.jsx(ot,{}),a.jsx(nt,{wrapperStyle:{fontSize:10}}),u.map(t=>a.jsx(it,{type:"monotone",dataKey:t,name:B[t]??t,stackId:"cores",stroke:H[t]??l.muted,fill:H[t]??l.muted,fillOpacity:t==="idle"?.15:.8,isAnimationActive:!1},t))]})}),h.length>0?a.jsxs(a.Fragment,{children:[a.jsx("ul",{className:"space-y-1 text-xs text-muted-foreground",children:h.map(t=>a.jsxs("li",{children:["Stage ",t.stageId,": ",Math.round(t.ratio*100),"% non-local (",t.nonLocalTasks,"/",t.taskCount," tasks)"]},t.stageId))}),a.jsx($,{page:s,totalPages:e,onPrev:()=>r(t=>t-1),onNext:()=>r(t=>t+1)})]}):null,v?a.jsx("p",{className:"text-xs text-muted-foreground",children:"Idle cores also flagged → see Memory Utilization widget"}):null]})})});export{gt as CoreUsageArea};
|