Commit pirate JS files

This commit is contained in:
2026-04-02 17:39:02 -06:00
parent 7b15a0eb9c
commit d287f8a443
49 changed files with 19149 additions and 0 deletions

View File

@@ -0,0 +1,593 @@
/*
Copyright (c) 2023-forever Douglas Malnati. All rights reserved.
See the /faq/tos page for details.
(If this generated header is stamped on a file which is a 3rd party file or under a different license or copyright, then ignore this copyright statement and use that file's terms.)
*/
import { Base } from './Base.js';
import { StrAccumulator } from '/js/Utl.js';
import { AsyncResourceLoader } from './AsyncResourceLoader.js';
export class WsprSearchUiStatsFilterController
extends Base
{
static urlEchartsScript = `https://fastly.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js`;
constructor(cfg)
{
super();
this.cfg = cfg;
this.ok =
this.cfg.container &&
this.cfg.wsprSearch;
this.chart = null;
this.chartReady = false;
this.chartLoadPromise = null;
this.renderSankeyToken = 0;
if (this.ok)
{
this.ui = this.MakeUI();
this.cfg.container.appendChild(this.ui);
this.EnsureChartReady();
}
else
{
this.Err(`WsprSearchUiStatsFilterController`, `Could not init`);
}
}
OnEvent(evt)
{
if (this.ok)
{
switch (evt.type) {
case "SEARCH_COMPLETE": this.OnSearchComplete(); break;
}
}
}
async OnSearchComplete()
{
let slotStatsList = this.ComputeSlotStats();
this.RenderText(slotStatsList);
this.ScheduleRenderSankey(slotStatsList);
}
GetFlowSpecList()
{
return [
{ label: "S0 Reg", slot: 0, type: "regular" },
{ label: "S0 ET", slot: 0, type: "telemetry" },
{ label: "S1 BT", slot: 1, telemetryType: "basic" },
{ label: "S1 ET", slot: 1, telemetryType: "extended" },
{ label: "S2 ET", slot: 2 },
{ label: "S3 ET", slot: 3 },
{ label: "S4 ET", slot: 4 },
];
}
MsgMatchesFlowSpec(msg, flowSpec)
{
if (!flowSpec.type)
{
if (!flowSpec.telemetryType)
{
return true;
}
}
if (flowSpec.telemetryType == "basic")
{
return msg.IsTelemetryBasic();
}
if (flowSpec.telemetryType == "extended")
{
return msg.IsTelemetryExtended();
}
return msg.type == flowSpec.type;
}
ComputeSlotStats()
{
let flowSpecList = this.GetFlowSpecList();
let slotStatsList = [];
for (const flowSpec of flowSpecList)
{
slotStatsList.push({
label: flowSpec.label,
flowSpec: flowSpec,
input: 0,
rejectedBadTelemetry: 0,
rejectedBySpec: 0,
rejectedByFingerprinting: 0,
finalCandidate: 0,
outcomeZeroCandidates: 0,
outcomeOneCandidate: 0,
outcomeMultiCandidate: 0,
});
}
this.cfg.wsprSearch.ForEachWindowMsgListList(msgListList => {
for (let idx = 0; idx < slotStatsList.length; ++idx)
{
let s = slotStatsList[idx];
let msgList = msgListList[s.flowSpec.slot];
let msgListFiltered = msgList.filter(msg => this.MsgMatchesFlowSpec(msg, s.flowSpec));
for (const msg of msgListFiltered)
{
s.input += 1;
if (msg.IsCandidate())
{
s.finalCandidate += 1;
continue;
}
let rejectType = this.GetRejectType(msg);
if (rejectType == "ByBadTelemetry")
{
s.rejectedBadTelemetry += 1;
}
else if (rejectType == "BySpec")
{
s.rejectedBySpec += 1;
}
else if (rejectType == "ByFingerprinting")
{
s.rejectedByFingerprinting += 1;
}
}
let candidateCount = 0;
for (const msg of msgListFiltered)
{
if (msg.IsCandidate())
{
candidateCount += 1;
}
}
if (candidateCount == 0)
{
// Keep Sankey math consistent (message units across all stages):
// zero-candidate windows contribute zero candidate messages.
s.outcomeZeroCandidates += 0;
}
else if (candidateCount == 1)
{
s.outcomeOneCandidate += 1;
}
else
{
// Multi-candidate windows contribute all remaining candidate messages.
s.outcomeMultiCandidate += candidateCount;
}
}
});
for (const s of slotStatsList)
{
s.afterBadTelemetry = s.input - s.rejectedBadTelemetry;
s.afterBySpec = s.afterBadTelemetry - s.rejectedBySpec;
s.afterByFingerprinting = s.afterBySpec - s.rejectedByFingerprinting;
if (s.afterByFingerprinting < 0)
{
s.afterByFingerprinting = 0;
}
}
return slotStatsList;
}
GetRejectType(msg)
{
let auditList = msg.candidateFilterAuditList || [];
if (auditList.length == 0)
{
return "";
}
// By design, first reject in pipeline determines final state.
return auditList[0].type || "";
}
RenderText(slotStatsList)
{
let a = new StrAccumulator();
let fmtPct = (num, den) => {
let pct = 0;
if (den > 0)
{
pct = Math.round((num / den) * 100);
}
return `(${pct.toString().padStart(3)}%)`;
};
a.A(`Filter Stats (Per Slot)`);
a.A(`-----------------------`);
for (let slot = 0; slot < slotStatsList.length; ++slot)
{
let s = slotStatsList[slot];
let valueWidth = Math.max(
1,
s.input.toString().length,
s.rejectedBadTelemetry.toString().length,
s.rejectedBySpec.toString().length,
s.rejectedByFingerprinting.toString().length,
s.finalCandidate.toString().length,
s.outcomeZeroCandidates.toString().length,
s.outcomeOneCandidate.toString().length,
s.outcomeMultiCandidate.toString().length,
);
let fmtVal = (v) => v.toString().padStart(valueWidth);
a.A(`${s.label}`);
a.A(` Input : ${s.input}`);
a.A(` Rejected BadTelemetry : ${fmtVal(s.rejectedBadTelemetry)} ${fmtPct(s.rejectedBadTelemetry, s.input)}`);
a.A(` Rejected BySpec : ${fmtVal(s.rejectedBySpec)} ${fmtPct(s.rejectedBySpec, s.input)}`);
a.A(` Rejected Fingerprint : ${fmtVal(s.rejectedByFingerprinting)} ${fmtPct(s.rejectedByFingerprinting, s.input)}`);
a.A(` Final Candidate : ${fmtVal(s.finalCandidate)} ${fmtPct(s.finalCandidate, s.input)}`);
a.A(` Outcome: 0 candidate : ${fmtVal(s.outcomeZeroCandidates)} ${fmtPct(s.outcomeZeroCandidates, s.input)}`);
a.A(` Outcome: 1 candidate : ${fmtVal(s.outcomeOneCandidate)} ${fmtPct(s.outcomeOneCandidate, s.input)}`);
a.A(` Outcome: 2+ candidates: ${fmtVal(s.outcomeMultiCandidate)} ${fmtPct(s.outcomeMultiCandidate, s.input)}`);
}
this.ta.value = a.Get();
}
async RenderSankey(slotStatsList)
{
await this.EnsureChartReady();
if (!this.chartReady)
{
return;
}
let nodeMap = new Map();
let links = [];
let addNode = (name, depth = undefined) => {
if (!nodeMap.has(name))
{
let node = { name };
if (depth != undefined)
{
node.depth = depth;
}
nodeMap.set(name, node);
return;
}
// Keep the earliest stage depth if the node was already created.
if (depth != undefined)
{
let node = nodeMap.get(name);
if (node.depth == undefined || depth < node.depth)
{
node.depth = depth;
}
}
};
let addLink = (source, target, value) => {
if (value <= 0)
{
return;
}
addNode(source);
addNode(target);
links.push({ source, target, value });
};
for (let slot = 0; slot < slotStatsList.length; ++slot)
{
let s = slotStatsList[slot];
let prefix = s.label;
let nInput = `${prefix}`;
let nAfterBad = `${prefix} After BadTelemetry`;
let nAfterSpec = `${prefix} After BySpec`;
let nAfterFp = `${prefix} After ByFingerprinting`;
let nRejBad = `${prefix} Rejected BadTelemetry`;
let nRejSpec = `${prefix} Rejected BySpec`;
let nRejFp = `${prefix} Rejected ByFingerprinting`;
let nOutcomeZero = `${prefix} Outcome: 0 Candidate`;
let nOutcomeOne = `${prefix} Outcome: 1 Candidate`;
let nOutcomeMulti = `${prefix} Outcome: 2+ Candidates`;
addNode(nInput, 0);
addNode(nRejBad, 1);
addNode(nAfterBad, 1);
addNode(nRejSpec, 2);
addNode(nAfterSpec, 2);
addNode(nRejFp, 3);
addNode(nAfterFp, 3);
addNode(nOutcomeZero, 4);
addNode(nOutcomeOne, 4);
addNode(nOutcomeMulti, 4);
addLink(nInput, nRejBad, s.rejectedBadTelemetry);
addLink(nInput, nAfterBad, s.afterBadTelemetry);
addLink(nAfterBad, nRejSpec, s.rejectedBySpec);
addLink(nAfterBad, nAfterSpec, s.afterBySpec);
addLink(nAfterSpec, nRejFp, s.rejectedByFingerprinting);
addLink(nAfterSpec, nAfterFp, s.afterByFingerprinting);
addLink(nAfterFp, nOutcomeZero, s.outcomeZeroCandidates);
addLink(nAfterFp, nOutcomeOne, s.outcomeOneCandidate);
addLink(nAfterFp, nOutcomeMulti, s.outcomeMultiCandidate);
}
this.chart.setOption({
title: {
text: "Filter Pipeline",
left: "left",
},
tooltip: {
trigger: "item",
triggerOn: "mousemove",
},
series: [
{
type: "sankey",
// Use automatic Sankey layout so each phase is positioned by graph depth.
layoutIterations: 64,
nodeAlign: "justify",
nodeGap: 16,
emphasis: {
// Custom hover behavior below handles upstream-only highlighting.
focus: "none",
},
data: Array.from(nodeMap.values()),
links: links,
lineStyle: {
color: "source",
curveness: 0.5,
},
},
],
animation: false,
}, true);
this.#InstallUpstreamHover(nodeMap, links);
}
ScheduleRenderSankey(slotStatsList)
{
let token = ++this.renderSankeyToken;
let run = async () => {
if (token != this.renderSankeyToken)
{
return;
}
await this.RenderSankey(slotStatsList);
};
if (window.requestIdleCallback)
{
window.requestIdleCallback(() => {
window.requestAnimationFrame(() => {
run();
});
}, { timeout: 250 });
}
else
{
window.setTimeout(() => {
window.requestAnimationFrame(() => {
run();
});
}, 0);
}
}
#InstallUpstreamHover(nodeMap, links)
{
if (!this.chart)
{
return;
}
let nodeNameList = Array.from(nodeMap.keys());
let nodeNameSet = new Set(nodeNameList);
let nodeIdxByName = new Map();
nodeNameList.forEach((name, idx) => nodeIdxByName.set(name, idx));
let incomingEdgeIdxByTarget = new Map();
for (let i = 0; i < links.length; ++i)
{
let l = links[i];
if (!incomingEdgeIdxByTarget.has(l.target))
{
incomingEdgeIdxByTarget.set(l.target, []);
}
incomingEdgeIdxByTarget.get(l.target).push(i);
}
// Track highlighted items so we can downplay cleanly.
this.sankeyHoverState = {
nodeIdxSet: new Set(),
edgeIdxSet: new Set(),
};
let clearHighlight = () => {
if (!this.sankeyHoverState)
{
return;
}
for (const idx of this.sankeyHoverState.nodeIdxSet)
{
this.chart.dispatchAction({
type: "downplay",
seriesIndex: 0,
dataType: "node",
dataIndex: idx,
});
}
for (const idx of this.sankeyHoverState.edgeIdxSet)
{
this.chart.dispatchAction({
type: "downplay",
seriesIndex: 0,
dataType: "edge",
dataIndex: idx,
});
}
this.sankeyHoverState.nodeIdxSet.clear();
this.sankeyHoverState.edgeIdxSet.clear();
};
let applyUpstreamHighlight = (seedNameList) => {
clearHighlight();
let seenNameSet = new Set();
let stack = [...seedNameList];
while (stack.length)
{
let cur = stack.pop();
if (!cur || seenNameSet.has(cur))
{
continue;
}
seenNameSet.add(cur);
let nodeIdx = nodeIdxByName.get(cur);
if (nodeIdx != undefined)
{
this.sankeyHoverState.nodeIdxSet.add(nodeIdx);
}
let edgeIdxList = incomingEdgeIdxByTarget.get(cur) || [];
for (const edgeIdx of edgeIdxList)
{
this.sankeyHoverState.edgeIdxSet.add(edgeIdx);
let src = links[edgeIdx].source;
if (src && !seenNameSet.has(src))
{
stack.push(src);
}
}
}
for (const idx of this.sankeyHoverState.nodeIdxSet)
{
this.chart.dispatchAction({
type: "highlight",
seriesIndex: 0,
dataType: "node",
dataIndex: idx,
});
}
for (const idx of this.sankeyHoverState.edgeIdxSet)
{
this.chart.dispatchAction({
type: "highlight",
seriesIndex: 0,
dataType: "edge",
dataIndex: idx,
});
}
};
this.chart.off("mouseover");
this.chart.off("globalout");
this.chart.on("mouseover", (params) => {
if (!params || params.seriesType != "sankey")
{
return;
}
if (params.dataType == "node")
{
let name = params?.data?.name;
if (nodeNameSet.has(name))
{
applyUpstreamHighlight([name]);
}
}
else if (params.dataType == "edge")
{
// Upstream of an edge means upstream of its target.
let target = params?.data?.target;
if (nodeNameSet.has(target))
{
applyUpstreamHighlight([target]);
}
}
});
this.chart.on("globalout", () => {
clearHighlight();
});
}
async EnsureChartReady()
{
if (this.chartReady)
{
return;
}
if (!this.chartLoadPromise)
{
this.chartLoadPromise = AsyncResourceLoader.AsyncLoadScript(WsprSearchUiStatsFilterController.urlEchartsScript);
}
try
{
await this.chartLoadPromise;
if (!this.chart)
{
this.chart = echarts.init(this.chartDiv);
}
this.chartReady = true;
}
catch (e)
{
this.Err(`WsprSearchUiStatsFilterController`, `Could not init chart: ${e}`);
}
}
MakeUI()
{
let ui = document.createElement('div');
this.ta = document.createElement('textarea');
this.ta.spellcheck = "false";
this.ta.readOnly = true;
this.ta.disabled = true;
this.ta.style.width = "600px";
this.ta.style.height = "260px";
this.chartDiv = document.createElement('div');
this.chartDiv.style.boxSizing = "border-box";
this.chartDiv.style.border = "1px solid black";
this.chartDiv.style.width = "1210px";
this.chartDiv.style.height = "800px";
this.chartDiv.style.marginTop = "8px";
ui.appendChild(this.ta);
ui.appendChild(this.chartDiv);
return ui;
}
}