Files
regex-tools/src/components/Pcre2TracePanel.tsx

408 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useRef, useState } from "react";
import { Pcre2TraceSupervisor } from "../regex/execution/Pcre2TraceSupervisor";
import { WorkerRequestError } from "../regex/execution/WorkerSupervisor";
import { DEFAULT_REGEX_LIMITS } from "../regex/execution/request-limits";
import type {
RegexEngineOptions,
RegexFlavourId,
} from "../regex/model/flavour";
import type { Pcre2TraceEvent, Pcre2TraceResult } from "../regex/model/trace";
import type { SourceRange } from "../regex/model/syntax";
const DEFAULT_TRACE_EVENTS = 5_000;
const DEFAULT_TRACE_BYTES = 2 * 1024 * 1024;
const MAXIMUM_RENDERED_TRACE_EVENTS = 2_000;
type TraceState =
| { readonly status: "idle"; readonly message: string }
| { readonly status: "running"; readonly message: string }
| { readonly status: "ready"; readonly message: string }
| { readonly status: "timeout"; readonly message: string }
| { readonly status: "error"; readonly message: string };
function movementLabel(event: Pcre2TraceEvent): string {
switch (event.movement.classification) {
case "first-event":
return "first";
case "same-position":
return "same position";
case "apparent-backtrack":
return "apparent backtrack";
case "forward":
return "forward";
}
}
export function Pcre2TracePanel({
active,
flavour,
pattern,
flags,
options,
subject,
timeoutMs,
onClose,
onSelectRanges,
}: {
readonly active: boolean;
readonly flavour: RegexFlavourId;
readonly pattern: string;
readonly flags: readonly string[];
readonly options: RegexEngineOptions;
readonly subject: string;
readonly timeoutMs: number;
readonly onClose: () => void;
readonly onSelectRanges: (
patternRange: SourceRange,
subjectRange: SourceRange,
) => void;
}) {
const supervisor = useRef<Pcre2TraceSupervisor | null>(null);
const requestRevision = useRef(0);
const [maximumEvents, setMaximumEvents] = useState(DEFAULT_TRACE_EVENTS);
const [maximumBytes, setMaximumBytes] = useState(DEFAULT_TRACE_BYTES);
const [completed, setCompleted] = useState<{
readonly result: Pcre2TraceResult;
readonly input: {
readonly pattern: string;
readonly subject: string;
readonly flags: string;
readonly options: string;
};
}>();
const [state, setState] = useState<TraceState>({
status: "idle",
message: "Run an isolated PCRE2 automatic-callout trace.",
});
const traceEngine = () => {
supervisor.current ??= new Pcre2TraceSupervisor();
return supervisor.current;
};
useEffect(() => {
return () => supervisor.current?.dispose();
}, []);
useEffect(() => {
if (active) return;
supervisor.current?.cancel();
}, [active]);
const limitsValid =
Number.isSafeInteger(maximumEvents) &&
maximumEvents >= 1 &&
maximumEvents <= DEFAULT_REGEX_LIMITS.maximumTraceEvents &&
Number.isSafeInteger(maximumBytes) &&
maximumBytes >= 32 &&
maximumBytes <= DEFAULT_REGEX_LIMITS.maximumTraceBytes;
const runTrace = async () => {
if (flavour !== "pcre2" || !limitsValid) return;
const revision = (requestRevision.current += 1);
setCompleted(undefined);
setState({
status: "running",
message: "Collecting reported callouts in a dedicated worker…",
});
try {
const next = await traceEngine().trace(
{
flavour,
pattern,
flags,
options,
subject,
maximumTraceEvents: maximumEvents,
maximumTraceBytes: maximumBytes,
},
timeoutMs,
);
if (revision !== requestRevision.current) return;
setCompleted({
result: next,
input: {
pattern,
subject,
flags: flags.join(""),
options: JSON.stringify(options),
},
});
const error = next.diagnostics.find(
(diagnostic) => diagnostic.severity === "error",
);
setState(
next.accepted
? {
status: "ready",
message: next.truncated
? "Bounded trace complete; collection stopped at a configured cap."
: "Bounded trace complete.",
}
: {
status: "error",
message: error?.message ?? "PCRE2 rejected the trace request.",
},
);
} catch (error) {
if (revision !== requestRevision.current) return;
if (error instanceof WorkerRequestError && error.kind === "timeout") {
setState({
status: "timeout",
message: `${error.message}; the trace worker was terminated.`,
});
} else if (
error instanceof WorkerRequestError &&
error.kind === "cancelled"
) {
setState({
status: "idle",
message: "Trace cancelled; its worker was terminated.",
});
} else {
setState({
status: "error",
message:
error instanceof Error
? error.message
: "The PCRE2 trace worker failed.",
});
}
}
};
const resultIsCurrent =
completed?.input.pattern === pattern &&
completed.input.subject === subject &&
completed.input.flags === flags.join("") &&
completed.input.options === JSON.stringify(options);
const visibleResult = resultIsCurrent ? completed.result : undefined;
const visibleState =
resultIsCurrent || state.status === "running"
? state
: {
status: "idle" as const,
message: "Inputs changed; run a new isolated trace.",
};
const renderedEvents =
visibleResult?.events.slice(0, MAXIMUM_RENDERED_TRACE_EVENTS) ?? [];
return (
<section className="panel trace-panel" aria-labelledby="trace-heading">
<header className="panel-heading">
<div>
<p className="eyebrow">PCRE2 10.47 · reported automatic callouts</p>
<h2 id="trace-heading">Execution trace</h2>
</div>
<button
type="button"
className="icon-button"
aria-label="Close execution trace"
data-dialog-initial-focus
onClick={onClose}
>
×
</button>
</header>
<p className="trace-disclosure">
This is an actual stream reported by PCRE2_AUTO_CALLOUT for one{" "}
<code>pcre2_match()</code> invocation. Movement labels are derived only
from adjacent reported subject positions; the stream is not a complete
record of every internal engine action.
</p>
<div className="trace-controls">
<label>
<span>Maximum events</span>
<input
type="number"
min={1}
max={DEFAULT_REGEX_LIMITS.maximumTraceEvents}
step={1}
value={maximumEvents}
onChange={(event) => setMaximumEvents(Number(event.target.value))}
/>
</label>
<label>
<span>Maximum serialized bytes</span>
<input
type="number"
min={32}
max={DEFAULT_REGEX_LIMITS.maximumTraceBytes}
step={1024}
value={maximumBytes}
onChange={(event) => setMaximumBytes(Number(event.target.value))}
/>
</label>
<button
type="button"
className="primary-button"
disabled={
flavour !== "pcre2" ||
!limitsValid ||
visibleState.status === "running"
}
onClick={() => void runTrace()}
>
{visibleState.status === "running" ? "Tracing…" : "Run trace"}
</button>
{visibleState.status === "running" ? (
<button
type="button"
className="secondary-button"
onClick={() => {
requestRevision.current += 1;
supervisor.current?.cancel();
setState({
status: "idle",
message: "Trace cancelled; its worker was terminated.",
});
}}
>
Cancel trace
</button>
) : null}
</div>
{flavour !== "pcre2" ? (
<p className="empty-state" role="status">
Select PCRE2 to enable its automatic-callout trace.
</p>
) : null}
<div
className={`trace-status status-${visibleState.status}`}
role="status"
aria-live="polite"
>
<strong>{visibleState.message}</strong>
{visibleResult?.accepted ? (
<small>
{visibleResult.events.length.toLocaleString()} complete retained
events of {visibleResult.totalEventCount.toLocaleString()} callback
event(s) observed before return ·{" "}
{visibleResult.traceBytes.toLocaleString()} serialized bytes · last
complete event{" "}
{visibleResult.lastCompleteEvent?.toLocaleString() ?? "none"} ·{" "}
native status {visibleResult.nativeMatchStatus} ·{" "}
{visibleResult.matched ? "matched" : "not matched or bounded stop"}{" "}
· {visibleResult.elapsedMs.toFixed(2)} ms
</small>
) : null}
</div>
{visibleResult?.diagnostics.length ? (
<ul className="trace-diagnostics">
{visibleResult.diagnostics.map((diagnostic) => (
<li
key={diagnostic.id}
className={`diagnostic-${diagnostic.severity}`}
>
<strong>{diagnostic.severity}</strong> {diagnostic.message}
</li>
))}
</ul>
) : null}
{visibleResult && visibleResult.events.length > renderedEvents.length ? (
<p className="render-limit-banner" role="status">
<strong>Trace viewer limited.</strong> Showing the first{" "}
{renderedEvents.length.toLocaleString()} of{" "}
{visibleResult.events.length.toLocaleString()} complete retained
events. The worker result remains bounded by both configured caps.
</p>
) : null}
{renderedEvents.length > 0 ? (
<div className="trace-table-scroll">
<table className="trace-table">
<thead>
<tr>
<th scope="col">Event</th>
<th scope="col">Callout</th>
<th scope="col">Pattern</th>
<th scope="col">Next item</th>
<th scope="col">Subject</th>
<th scope="col">Captures</th>
<th scope="col">Mark</th>
<th scope="col">Movement</th>
</tr>
</thead>
<tbody>
{renderedEvents.map((event) => (
<tr key={event.eventNumber}>
<td>
<button
type="button"
className="trace-event-select"
aria-label={`Select trace event ${event.eventNumber}`}
onClick={() =>
onSelectRanges(
{
startUtf16:
event.reported.nextPatternItem.startUtf16,
endUtf16: event.reported.nextPatternItem.endUtf16,
},
{
startUtf16: event.reported.subjectPosition.utf16,
endUtf16: event.reported.subjectPosition.utf16,
},
)
}
>
{event.eventNumber}
</button>
</td>
<td>{event.reported.calloutNumber}</td>
<td>
{event.reported.patternPosition.utf16} UTF-16
<small>
{event.reported.patternPosition.nativeByte} byte
</small>
</td>
<td>
<code>
{pattern.slice(
event.reported.nextPatternItem.startUtf16,
event.reported.nextPatternItem.endUtf16,
) || "end"}
</code>
<small>
{event.reported.nextPatternItem.startNativeByte}
{event.reported.nextPatternItem.endNativeByte} bytes
</small>
</td>
<td>
{event.reported.subjectPosition.utf16} UTF-16
<small>
{event.reported.subjectPosition.nativeByte} byte
</small>
</td>
<td>
top {event.reported.captureTop} · last{" "}
{event.reported.captureLast}
</td>
<td>
{event.reported.mark ? (
<code>{event.reported.mark}</code>
) : (
"—"
)}
</td>
<td>
<span
className={`trace-movement trace-${event.movement.classification}`}
title={event.movement.explanation}
>
{movementLabel(event)}
</span>
<small>
{event.movement.subjectDeltaBytes > 0 ? "+" : ""}
{event.movement.subjectDeltaBytes} bytes · derived
</small>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : visibleResult?.accepted ? (
<p className="empty-state">No callout event was retained.</p>
) : null}
</section>
);
}