feat: refactor performance collection
Some checks failed
Deploy to GitHub Pages / build_site (push) Has been cancelled

This commit is contained in:
max_richter 2024-04-26 17:57:32 +02:00
parent 6eaecef35d
commit 2f0022f912
7 changed files with 306 additions and 173 deletions

View File

@ -1,9 +1,32 @@
<script lang="ts"> <script lang="ts">
export let points: number[]; export let points: number[];
$: max = Math.max(...points); export let type = "ms";
$: min = Math.min(...points); export let title = "Performance";
export let max: number | undefined = undefined;
export let min: number | undefined = undefined;
function getMax(m?: number) {
if (type === "%") {
return 100;
}
if (m !== undefined) {
if (m < 1) {
return Math.floor(m * 100) / 100;
}
if (m < 10) {
return Math.floor(m * 10) / 10;
}
return Math.floor(m);
}
return 1;
}
function constructPath() { function constructPath() {
max = max !== undefined ? max : Math.max(...points);
min = min !== undefined ? min : Math.min(...points);
return points return points
.map((point, i) => { .map((point, i) => {
const x = (i / (points.length - 1)) * 100; const x = (i / (points.length - 1)) * 100;
@ -15,11 +38,13 @@
</script> </script>
<div class="wrapper"> <div class="wrapper">
<p>Runtime Execution</p> <p>{title}</p>
<span class="min">{min}ms</span> <span class="min">{Math.floor(min || 0)}{type}</span>
<span class="max">{max}ms</span> <span class="max">{getMax(max)}{type}</span>
<svg preserveAspectRatio="none" viewBox="0 0 100 100"> <svg preserveAspectRatio="none" viewBox="0 0 100 100">
<polyline vector-effect="non-scaling-stroke" points={constructPath()} /> {#key points}
<polyline vector-effect="non-scaling-stroke" points={constructPath()} />
{/key}
</svg> </svg>
</div> </div>

View File

@ -1,168 +1,265 @@
<script lang="ts"> <script lang="ts">
import { browser } from "$app/environment";
import Monitor from "./Monitor.svelte"; import Monitor from "./Monitor.svelte";
import { humanizeNumber } from "$lib/helpers"; import { humanizeNumber } from "$lib/helpers";
import { Checkbox } from "@nodes/ui"; import { Checkbox } from "@nodes/ui";
import localStore from "$lib/helpers/localStore";
type PerformanceData = { import { type PerformanceData } from "./store";
total: Record<string, number>;
runs: Record<string, number[]>[];
};
export let data: PerformanceData; export let data: PerformanceData;
export let viewer: PerformanceData;
let lastRunOnly = true; let activeType = localStore<string>("nodes.performance.active-type", "total");
let showAverage = true;
function getAverage(key: string) {
return (
data
.map((run) => run[key]?.[0])
.filter((v) => v !== undefined)
.reduce((acc, run) => acc + run, 0) / data.length
);
}
function round(v: number) {
if (v < 1) {
return Math.floor(v * 100) / 100;
}
if (v < 10) {
return Math.floor(v * 10) / 10;
}
return Math.floor(v);
}
function getAverages() {
let lastRun = data.at(-1);
if (!lastRun) return {};
return Object.keys(lastRun).reduce(
(acc, key) => {
acc[key] = getAverage(key);
return acc;
},
{} as Record<string, number>,
);
}
function getLast(key: string) {
return data.at(-1)?.[key][0] || 0;
}
function getLasts() {
return data.at(-1) || {};
}
function getTotalPerformance(onlyLast = false) { function getTotalPerformance(onlyLast = false) {
if (onlyLast) { if (onlyLast) {
return ( return (
data.runs.at(-1).runtime[0] + viewer.runs.at(-1)["create-geometries"][0] getLast("runtime") +
getLast("create-geometries") +
getLast("worker-transfer")
); );
} }
return data.total.runtime + viewer.total["create-geometries"]; return (
} getAverage("runtime") +
getAverage("create-geometries") +
function count(input?: number | number[]) { getAverage("worker-transfer")
if (!input) return 0; );
if (Array.isArray(input))
return input.reduce((acc, val) => acc + val, 0) / input.length;
return input;
} }
function getCacheRatio(onlyLast = false) { function getCacheRatio(onlyLast = false) {
let ratio = onlyLast let ratio = onlyLast ? getLast("cache-hit") : getAverage("cache-hit");
? count(data.runs.at(-1)?.["cache-hit"])
: count(data.total["cache-hit"]);
return `${Math.floor(ratio * 100)}%`; return Math.floor(ratio * 100);
} }
const viewerKeys = [
"total-vertices",
"total-faces",
"create-geometries",
"split-result",
];
function getPerformanceData(onlyLast: boolean = false) { function getPerformanceData(onlyLast: boolean = false) {
if (onlyLast) { let data = onlyLast ? getLasts() : getAverages();
return Object.entries(data.runs.at(-1))
.map(([key, value]) => [key, value[0]]) return Object.entries(data)
.filter(
([key]) =>
!key.startsWith("node/") &&
key !== "total" &&
!key.includes("cache"),
)
.sort((a, b) => b[1] - a[1]);
}
return Object.entries(data.total)
.sort((a, b) => b[1] - a[1])
.filter( .filter(
([key]) => ([key]) =>
!key.startsWith("node/") && key !== "total" && !key.includes("cache"), !key.startsWith("node/") &&
); key !== "total" &&
!key.includes("cache") &&
!viewerKeys.includes(key),
)
.sort((a, b) => b[1] - a[1]);
} }
function getNodePerformanceData(onlyLast: boolean = false) { function getNodePerformanceData(onlyLast: boolean = false) {
if (onlyLast) { let data = onlyLast ? getLasts() : getAverages();
return Object.entries(data.runs.at(-1))
.map(([key, value]) => [key, value[0]]) return Object.entries(data)
.filter(([key]) => key.startsWith("node/"))
.sort((a, b) => b[1] - a[1]);
}
return Object.entries(data.total)
.filter(([key]) => key.startsWith("node/")) .filter(([key]) => key.startsWith("node/"))
.sort((a, b) => b[1] - a[1]); .sort((a, b) => b[1] - a[1]);
} }
function getViewerPerformanceData(onlyLast: boolean = false) { function getViewerPerformanceData(onlyLast: boolean = false) {
if (onlyLast) { let data = onlyLast ? getLasts() : getAverages();
return Object.entries(viewer.runs.at(-1)) return Object.entries(data)
.map(([key, value]) => [key, value[0]]) .filter(
.filter(([key]) => key !== "total-vertices" && key !== "total-faces") ([key]) =>
.sort((a, b) => b[1] - a[1]); key !== "total-vertices" &&
} key !== "total-faces" &&
return Object.entries(viewer.total) viewerKeys.includes(key),
.filter(([key]) => key !== "total-vertices" && key !== "total-faces") )
.sort((a, b) => b[1] - a[1]); .sort((a, b) => b[1] - a[1]);
} }
function constructPoints(key: keyof (typeof data.runs)[0]) { function getTotalPoints() {
return data.runs.map((run, i) => { if (showAverage) {
return run[key][0]; return data.map((run) => {
return (
run["runtime"].reduce((acc, v) => acc + v, 0) +
run["create-geometries"].reduce((acc, v) => acc + v, 0) +
run["worker-transfer"].reduce((acc, v) => acc + v, 0)
);
});
}
return data.map((run) => {
return (
run["runtime"][0] +
run["create-geometries"][0] +
run["worker-transfer"][0]
);
}); });
} }
function constructPoints(key: string) {
if (key === "total") {
return getTotalPoints();
}
return data.map((run) => {
if (key in run) {
if (showAverage) {
return run[key].reduce((acc, v) => acc + v, 0) / run[key].length;
} else {
return run[key][0];
}
}
return 0;
});
}
function getTitle(t: string) {
if (t.includes("/")) {
return `Node ${t.split("/").slice(-1).join("/")}`;
}
return t
.split("-")
.map((v) => v[0].toUpperCase() + v.slice(1))
.join(" ");
}
</script> </script>
{#key data} {#key $activeType && data}
{#if browser} {#if $activeType === "cache-hit"}
<Monitor points={constructPoints("runtime")} /> <Monitor
title="Cache Hits"
points={constructPoints($activeType)}
min={0}
max={1}
type="%"
/>
{:else}
<Monitor
title={getTitle($activeType)}
points={constructPoints($activeType)}
/>
{/if} {/if}
<div class="p-4"> <div class="p-4">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox id="show-total" bind:value={lastRunOnly} /> <Checkbox id="show-total" bind:value={showAverage} />
<label for="show-total">Show Average</label> <label for="show-total">Show Average</label>
</div> </div>
{#if data.runs.length !== 0} {#if data.length !== 0}
<h3>General</h3> <h3>General</h3>
<table> <table>
<tr> <tr>
<td> <td>
{Math.floor(getTotalPerformance(!lastRunOnly) * 100) / 100}<span {round(getTotalPerformance(!showAverage))}<span>ms</span>
>ms</span </td>
<td
class:active={$activeType === "total"}
on:click={() => ($activeType = "total")}
>
total<span
>({Math.floor(1000 / getTotalPerformance(showAverage))}fps)</span
> >
</td> </td>
<td>total</td>
</tr> </tr>
{#each getPerformanceData(!lastRunOnly) as [key, value]} {#each getPerformanceData(!showAverage) as [key, value]}
<tr> <tr>
<td> <td>
{Math.floor(value * 100) / 100}<span>ms</span> {round(value)}<span>ms</span>
</td> </td>
<td> <td
class:active={$activeType === key}
on:click={() => ($activeType = key)}
>
{key} {key}
</td> </td>
</tr> </tr>
{/each} {/each}
<tr> <tr>
<td> {getCacheRatio(!lastRunOnly)} </td> <td>{data.length}</td>
<td>cache hit</td>
</tr>
<tr>
<td>{data.runs.length}</td>
<td>Samples</td> <td>Samples</td>
</tr> </tr>
<h3>Nodes</h3> <h3>Nodes</h3>
{#each getNodePerformanceData(!lastRunOnly) as [key, value]} <tr>
<td> {getCacheRatio(!showAverage)}<span>%</span> </td>
<td
class:active={$activeType === "cache-hit"}
on:click={() => ($activeType = "cache-hit")}>cache hits</td
>
</tr>
{#each getNodePerformanceData(!showAverage) as [key, value]}
<tr> <tr>
<td> <td>
{Math.floor(value * 100) / 100}<span>ms</span> {round(value)}<span>ms</span>
</td> </td>
<td>
<td
class:active={$activeType === key}
on:click={() => ($activeType = key)}
>
{key.split("/").slice(-1).join("/")} {key.split("/").slice(-1).join("/")}
</td> </td>
</tr> </tr>
{/each} {/each}
{#if viewer.runs.length} <h3>Viewer</h3>
<h3>Viewer</h3> <tr>
<td>{humanizeNumber(getLast("total-vertices"))}</td>
<td>Vertices</td>
</tr>
<tr>
<td>{humanizeNumber(getLast("total-faces"))}</td>
<td>Faces</td>
</tr>
{#each getViewerPerformanceData(!showAverage) as [key, value]}
<tr> <tr>
<td>{humanizeNumber(viewer.runs.at(-1)?.["total-vertices"])}</td> <td>
<td>Vertices</td> {round(value)}<span>ms</span>
</td>
<td
class:active={$activeType === key}
on:click={() => ($activeType = key)}
>
{key.split("/").slice(-1).join("/")}
</td>
</tr> </tr>
<tr> {/each}
<td>{humanizeNumber(viewer.runs.at(-1)?.["total-faces"])}</td>
<td>Faces</td>
</tr>
{#each getViewerPerformanceData(!lastRunOnly) as [key, value]}
<tr>
<td>
{Math.floor(value * 100) / 100}<span>ms</span>
</td>
<td>
{key.split("/").slice(-1).join("/")}
</td>
</tr>
{/each}
{/if}
</table> </table>
{:else} {:else}
<p>No runs available</p> <p>No runs available</p>
@ -185,6 +282,9 @@
padding-right: 10px; padding-right: 10px;
padding-block: 5px; padding-block: 5px;
} }
td.active {
font-weight: bold;
}
tr > td:nth-child(1) { tr > td:nth-child(1) {
text-align: right; text-align: right;
} }

View File

@ -1,26 +1,24 @@
import { readable, type Readable } from "svelte/store"; import { readable, type Readable } from "svelte/store";
export type PerformanceData = { export type PerformanceData = Record<string, number[]>[];
total: Record<string, number>;
runs: Record<string, number[]>[];
}
export interface PerformanceStore extends Readable<PerformanceData> { export interface PerformanceStore extends Readable<PerformanceData> {
startRun(): void; startRun(): void;
stopRun(): void; stopRun(): void;
addPoint(name: string, value?: number): void; addPoint(name: string, value?: number): void;
mergeData(data: PerformanceData[number]): void;
get: () => PerformanceData; get: () => PerformanceData;
} }
export function createPerformanceStore(): PerformanceStore { export function createPerformanceStore(): PerformanceStore {
let data: PerformanceData = { total: {}, runs: [] }; let data: PerformanceData = [];
let currentRun: Record<string, number[]> | undefined; let currentRun: Record<string, number[]> | undefined;
let set: (v: PerformanceData) => void; let set: (v: PerformanceData) => void;
const { subscribe } = readable<PerformanceData>({ total: {}, runs: [] }, (_set) => { const { subscribe } = readable<PerformanceData>([], (_set) => {
set = _set; set = _set;
}); });
@ -30,19 +28,8 @@ export function createPerformanceStore(): PerformanceStore {
function stopRun() { function stopRun() {
if (currentRun) { if (currentRun) {
// Calculate total data.push(currentRun);
Object.keys(currentRun).forEach((name) => { data = data.slice(-100);
if (!currentRun?.[name]?.length) return;
let runTotal = currentRun[name].reduce((a, b) => a + b, 0) / currentRun[name].length;
if (!data.total[name]) {
data.total[name] = runTotal;
} else {
data.total[name] = (data.total[name] + runTotal) / 2;
}
});
data.runs.push(currentRun);
data.runs = data.runs.slice(-100);
currentRun = undefined; currentRun = undefined;
if (set) set(data); if (set) set(data);
} }
@ -58,11 +45,26 @@ export function createPerformanceStore(): PerformanceStore {
return data; return data;
} }
function mergeData(newData: PerformanceData[number]) {
let r = currentRun;
if (!r) return;
Object.keys(newData).forEach((name) => {
if (name in r) {
r[name].push(...newData[name]);
} else {
r[name] = newData[name];
}
});
}
return { return {
subscribe, subscribe,
startRun, startRun,
stopRun, stopRun,
addPoint, addPoint,
mergeData,
get get
} }
} }

View File

@ -17,9 +17,6 @@
let geometries: BufferGeometry[] = []; let geometries: BufferGeometry[] = [];
let lines: Vector3[][] = []; let lines: Vector3[][] = [];
let totalVertices = 0;
let totalFaces = 0;
function fastArrayHash(arr: ArrayBuffer) { function fastArrayHash(arr: ArrayBuffer) {
let ints = new Uint8Array(arr); let ints = new Uint8Array(arr);
@ -41,14 +38,11 @@
geometry = new BufferGeometry(), geometry = new BufferGeometry(),
): BufferGeometry { ): BufferGeometry {
// Extract data from the encoded array // Extract data from the encoded array
let index = 0; let index = 1;
const geometryType = encodedData[index++]; // const geometryType = encodedData[index++];
const vertexCount = encodedData[index++]; const vertexCount = encodedData[index++];
const faceCount = encodedData[index++]; const faceCount = encodedData[index++];
totalVertices += vertexCount;
totalFaces += faceCount;
// Indices // Indices
const indicesEnd = index + faceCount * 3; const indicesEnd = index + faceCount * 3;
const indices = encodedData.subarray(index, indicesEnd); const indices = encodedData.subarray(index, indicesEnd);
@ -62,10 +56,24 @@
); );
index = index + vertexCount * 3; index = index + vertexCount * 3;
let hash = fastArrayHash(vertices); let hash = fastArrayHash(vertices);
let posAttribute = geometry.getAttribute(
"position",
) as BufferAttribute | null;
if (geometry.userData?.hash === hash) { if (geometry.userData?.hash === hash) {
return geometry; return geometry;
} }
if (posAttribute && posAttribute.count === vertexCount) {
posAttribute.set(vertices, 0);
posAttribute.needsUpdate = true;
} else {
geometry.setAttribute(
"position",
new Float32BufferAttribute(vertices, 3),
);
}
const normals = new Float32Array( const normals = new Float32Array(
encodedData.buffer, encodedData.buffer,
index * 4, index * 4,
@ -81,20 +89,6 @@
geometry.setIndex([...indices]); geometry.setIndex([...indices]);
} }
let posAttribute = geometry.getAttribute(
"position",
) as BufferAttribute | null;
if (posAttribute && posAttribute.count === vertexCount) {
posAttribute.set(vertices, 0);
posAttribute.needsUpdate = true;
} else {
geometry.setAttribute(
"position",
new Float32BufferAttribute(vertices, 3),
);
}
const normalsAttribute = geometry.getAttribute( const normalsAttribute = geometry.getAttribute(
"normal", "normal",
) as BufferAttribute | null; ) as BufferAttribute | null;
@ -172,15 +166,14 @@
return positions; return positions;
} }
export const update = function updateGeometries(result: Int32Array) { export let result: Int32Array;
$: result && updateGeometries();
function updateGeometries() {
let a = performance.now(); let a = performance.now();
const inputs = parse_args(result); const inputs = parse_args(result);
let b = performance.now(); let b = performance.now();
perf?.addPoint("split-result", b - a); perf?.addPoint("split-result", b - a);
totalVertices = 0;
totalFaces = 0;
if ($AppSettings.showStemLines) { if ($AppSettings.showStemLines) {
a = performance.now(); a = performance.now();
lines = inputs lines = inputs
@ -194,11 +187,17 @@
perf?.addPoint("create-lines", b - a); perf?.addPoint("create-lines", b - a);
} }
let totalVertices = 0;
let totalFaces = 0;
a = performance.now(); a = performance.now();
geometries = inputs geometries = inputs
.map((input, i) => { .map((input, i) => {
if (input[0] === 1) { if (input[0] === 1) {
return createGeometryFromEncodedData(input, geometries[i]); let geo = createGeometryFromEncodedData(input);
totalVertices += geo.userData.vertexCount;
totalFaces += geo.userData.faceCount;
return geo;
} }
}) })
.filter(Boolean) as BufferGeometry[]; .filter(Boolean) as BufferGeometry[];
@ -206,7 +205,7 @@
perf?.addPoint("create-geometries", b - a); perf?.addPoint("create-geometries", b - a);
perf?.addPoint("total-vertices", totalVertices); perf?.addPoint("total-vertices", totalVertices);
perf?.addPoint("total-faces", totalFaces); perf?.addPoint("total-faces", totalFaces);
}; }
</script> </script>
<Canvas> <Canvas>

View File

@ -45,7 +45,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
perf?: PerformanceStore; perf?: PerformanceStore;
constructor(private registry: NodeRegistry, private cache?: RuntimeCache) { } constructor(private registry: NodeRegistry, private cache?: RuntimeCache<Int32Array>) { }
private async getNodeDefinitions(graph: Graph) { private async getNodeDefinitions(graph: Graph) {
@ -215,6 +215,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
let inputHash = `node-${node.id}-${fastHashArrayBuffer(encoded_inputs)}`; let inputHash = `node-${node.id}-${fastHashArrayBuffer(encoded_inputs)}`;
b = performance.now(); b = performance.now();
this.perf?.addPoint("hash-inputs", b - a); this.perf?.addPoint("hash-inputs", b - a);
let cachedValue = this.cache?.get(inputHash); let cachedValue = this.cache?.get(inputHash);
if (cachedValue !== undefined) { if (cachedValue !== undefined) {
log.log(`Using cached value for ${node_type.id || node.id}`); log.log(`Using cached value for ${node_type.id || node.id}`);
@ -253,6 +254,10 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
this.perf?.stopRun(); this.perf?.stopRun();
if (this.cache) {
this.cache.size = sortedNodes.length * 2;
}
return res as unknown as Int32Array; return res as unknown as Int32Array;
} }
@ -261,15 +266,18 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
export class MemoryRuntimeCache implements RuntimeCache { export class MemoryRuntimeCache implements RuntimeCache {
private cache: Record<string, unknown> = {}; private cache: [string, unknown][] = [];
size = 50;
get<T>(key: string): T | undefined { get<T>(key: string): T | undefined {
return this.cache[key] as T; return this.cache.find(([k]) => k === key)?.[1] as T;
} }
set<T>(key: string, value: T): void { set<T>(key: string, value: T): void {
this.cache[key] = value; this.cache.push([key, value]);
this.cache = this.cache.slice(-this.size);
} }
clear(): void { clear(): void {
this.cache = {}; this.cache = [];
} }
} }

View File

@ -20,18 +20,15 @@
import GraphSettings from "$lib/settings/panels/GraphSettings.svelte"; import GraphSettings from "$lib/settings/panels/GraphSettings.svelte";
import NestedSettings from "$lib/settings/panels/NestedSettings.svelte"; import NestedSettings from "$lib/settings/panels/NestedSettings.svelte";
import { createPerformanceStore } from "$lib/performance"; import { createPerformanceStore } from "$lib/performance";
import { type PerformanceData } from "$lib/performance/store";
const nodeRegistry = new RemoteNodeRegistry(""); const nodeRegistry = new RemoteNodeRegistry("");
const workerRuntime = new WorkerRuntimeExecutor(); const workerRuntime = new WorkerRuntimeExecutor();
let performanceData: PerformanceData; let performanceStore = createPerformanceStore();
let viewerPerformance = createPerformanceStore();
let res: Int32Array;
let activeNode: Node | undefined; let activeNode: Node | undefined;
let updateViewer: (arg: Int32Array) => void; let graphResult: Int32Array;
let graph = localStorage.getItem("graph") let graph = localStorage.getItem("graph")
? JSON.parse(localStorage.getItem("graph")!) ? JSON.parse(localStorage.getItem("graph")!)
@ -69,23 +66,21 @@
try { try {
let a = performance.now(); let a = performance.now();
// res = await remoteRuntime.execute(_graph, _settings); // res = await remoteRuntime.execute(_graph, _settings);
let res = await workerRuntime.execute(_graph, _settings); graphResult = await workerRuntime.execute(_graph, _settings);
let b = performance.now(); let b = performance.now();
updateViewer(res);
let perfData = await workerRuntime.getPerformanceData(); let perfData = await workerRuntime.getPerformanceData();
let lastRun = perfData.runs?.at(-1); let lastRun = perfData.at(-1);
if (lastRun) { if (lastRun) {
perfData.total["worker-transfer"] = b - a - lastRun.runtime[0];
lastRun["worker-transfer"] = [b - a - lastRun.runtime[0]]; lastRun["worker-transfer"] = [b - a - lastRun.runtime[0]];
performanceStore.mergeData(lastRun);
} }
performanceData = perfData;
isWorking = false; isWorking = false;
} catch (error) { } catch (error) {
console.log("errors", error); console.log("errors", error);
} }
viewerPerformance.stopRun(); performanceStore.stopRun();
viewerPerformance.startRun(); performanceStore.startRun();
if (unfinished) { if (unfinished) {
let d = unfinished; let d = unfinished;
@ -115,8 +110,8 @@
<Grid.Row> <Grid.Row>
<Grid.Cell> <Grid.Cell>
<Viewer <Viewer
bind:update={updateViewer} result={graphResult}
perf={viewerPerformance} perf={performanceStore}
centerCamera={$AppSettings.centerCamera} centerCamera={$AppSettings.centerCamera}
/> />
</Grid.Cell> </Grid.Cell>
@ -168,11 +163,8 @@
hidden={!$AppSettings.showPerformancePanel} hidden={!$AppSettings.showPerformancePanel}
icon="i-tabler-brand-speedtest" icon="i-tabler-brand-speedtest"
> >
{#if performanceData} {#if $performanceStore}
<PerformanceViewer <PerformanceViewer data={$performanceStore} />
data={performanceData}
viewer={$viewerPerformance}
/>
{/if} {/if}
</Panel> </Panel>
<Panel <Panel

View File

@ -36,19 +36,26 @@ export interface RuntimeExecutor {
execute: (graph: Graph, settings: Record<string, unknown>) => Promise<Int32Array>; execute: (graph: Graph, settings: Record<string, unknown>) => Promise<Int32Array>;
} }
export interface RuntimeCache { export interface RuntimeCache<T = unknown> {
/**
* The maximum number of items that can be stored in the cache
* @remarks When the cache size exceeds this value, the oldest items should be removed
*/
size: number;
/** /**
* Get the value for the given key * Get the value for the given key
* @param key - The key to get the value for * @param key - The key to get the value for
* @returns The value for the given key, or undefined if no such value exists * @returns The value for the given key, or undefined if no such value exists
*/ */
get: (key: string) => unknown | undefined; get: (key: string) => T | undefined;
/** /**
* Set the value for the given key * Set the value for the given key
* @param key - The key to set the value for * @param key - The key to set the value for
* @param value - The value to set * @param value - The value to set
*/ */
set: (key: string, value: unknown) => void; set: (key: string, value: T) => void;
/** /**
* Clear the cache * Clear the cache
*/ */