mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
refactor(perf): expose serializable renderer RSS capture
This commit is contained in:
1 parent
e27685dc5d
commit
5aab81e2cb
2 files changed
+247
-150
No files matched your search
@@ -2,6 +2,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import * as rendererProcessRssCaptureModule from './renderer-process-rss-capture';
|
||||
import {
|
||||
captureRendererProcessRssSample,
|
||||
createRendererProcessRssCapture,
|
||||
@@ -12,6 +13,34 @@ import {
|
||||
const RENDERER_PID = 42;
|
||||
const RENDERER_CREATION_TIME = 1_721_234_567_890;
|
||||
|
||||
test('exposes a self-contained factory for the Electron main-process injection boundary', () => {
|
||||
const factory = (
|
||||
rendererProcessRssCaptureModule as unknown as Record<string, unknown>
|
||||
)['createRendererProcessRssCaptureApi'];
|
||||
|
||||
assert.equal(typeof factory, 'function');
|
||||
const source = (factory as () => unknown).toString();
|
||||
const restoredFactory = Function(
|
||||
`"use strict"; return (${source});`
|
||||
)() as () => {
|
||||
create(
|
||||
webContentsOsPid: unknown,
|
||||
processMetrics: readonly unknown[]
|
||||
): RendererProcessRssCapture;
|
||||
};
|
||||
|
||||
assert.deepEqual(restoredFactory().create(RENDERER_PID, [metric()]), {
|
||||
identity: {
|
||||
creationTime: RENDERER_CREATION_TIME,
|
||||
pid: RENDERER_PID,
|
||||
},
|
||||
missingSampleCount: 0,
|
||||
peakRssBytes: 1_263_616,
|
||||
unavailableReason: null,
|
||||
validSampleCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
interface MetricOverrides {
|
||||
readonly creationTime?: unknown;
|
||||
readonly pid?: unknown;
|
||||
|
||||
@@ -21,132 +21,237 @@ export interface RendererProcessRssCapture {
|
||||
readonly validSampleCount: number;
|
||||
}
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
const KIBIBYTE_BYTES = 1_024;
|
||||
const MAX_WORKING_SET_KIB = Math.floor(
|
||||
Number.MAX_SAFE_INTEGER / KIBIBYTE_BYTES
|
||||
);
|
||||
|
||||
function readRecord(value: unknown): JsonRecord | null {
|
||||
return typeof value === 'object' && value !== null
|
||||
? (value as JsonRecord)
|
||||
: null;
|
||||
export interface RendererProcessRssCaptureApi {
|
||||
readonly create: (
|
||||
webContentsOsPid: unknown,
|
||||
processMetrics: readonly unknown[]
|
||||
) => RendererProcessRssCapture;
|
||||
readonly sample: (
|
||||
capture: RendererProcessRssCapture,
|
||||
webContentsOsPid: unknown,
|
||||
processMetrics: readonly unknown[]
|
||||
) => RendererProcessRssCapture;
|
||||
}
|
||||
|
||||
function isValidOsPid(value: unknown): value is number {
|
||||
return Number.isSafeInteger(value) && Number(value) > 0;
|
||||
}
|
||||
export function createRendererProcessRssCaptureApi(): RendererProcessRssCaptureApi {
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function findPidCandidates(
|
||||
processMetrics: readonly unknown[],
|
||||
pid: number
|
||||
): JsonRecord[] {
|
||||
return processMetrics
|
||||
.map((metric) => readRecord(metric))
|
||||
.filter(
|
||||
(metric): metric is JsonRecord =>
|
||||
metric !== null && metric['pid'] === pid
|
||||
);
|
||||
}
|
||||
const KIBIBYTE_BYTES = 1_024;
|
||||
const MAX_WORKING_SET_KIB = Math.floor(
|
||||
Number.MAX_SAFE_INTEGER / KIBIBYTE_BYTES
|
||||
);
|
||||
|
||||
function readMetric(metric: JsonRecord):
|
||||
| {
|
||||
readonly creationTime: number;
|
||||
readonly rssBytes: number;
|
||||
}
|
||||
| RendererProcessRssUnavailableReason {
|
||||
if (metric['type'] !== 'Tab' && metric['type'] !== 'Renderer') {
|
||||
return 'renderer-process-metric-type-invalid';
|
||||
}
|
||||
const helpers = {
|
||||
readRecord(value: unknown): JsonRecord | null {
|
||||
return typeof value === 'object' && value !== null
|
||||
? (value as JsonRecord)
|
||||
: null;
|
||||
},
|
||||
|
||||
const creationTime = metric['creationTime'];
|
||||
if (
|
||||
typeof creationTime !== 'number' ||
|
||||
!Number.isFinite(creationTime) ||
|
||||
creationTime < 0
|
||||
) {
|
||||
return 'renderer-process-metric-creation-time-invalid';
|
||||
}
|
||||
isValidOsPid(value: unknown): value is number {
|
||||
return Number.isSafeInteger(value) && Number(value) > 0;
|
||||
},
|
||||
|
||||
const memory = readRecord(metric['memory']);
|
||||
const workingSetSize = memory?.['workingSetSize'];
|
||||
if (
|
||||
!Number.isSafeInteger(workingSetSize) ||
|
||||
Number(workingSetSize) <= 0 ||
|
||||
Number(workingSetSize) > MAX_WORKING_SET_KIB
|
||||
) {
|
||||
return 'renderer-process-metric-working-set-invalid';
|
||||
}
|
||||
findPidCandidates(
|
||||
processMetrics: readonly unknown[],
|
||||
pid: number
|
||||
): JsonRecord[] {
|
||||
return processMetrics
|
||||
.map((metric) => helpers.readRecord(metric))
|
||||
.filter(
|
||||
(metric): metric is JsonRecord =>
|
||||
metric !== null && metric['pid'] === pid
|
||||
);
|
||||
},
|
||||
|
||||
return {
|
||||
creationTime,
|
||||
rssBytes: Number(workingSetSize) * KIBIBYTE_BYTES,
|
||||
readMetric(metric: JsonRecord):
|
||||
| {
|
||||
readonly creationTime: number;
|
||||
readonly rssBytes: number;
|
||||
}
|
||||
| RendererProcessRssUnavailableReason {
|
||||
if (metric['type'] !== 'Tab' && metric['type'] !== 'Renderer') {
|
||||
return 'renderer-process-metric-type-invalid';
|
||||
}
|
||||
|
||||
const creationTime = metric['creationTime'];
|
||||
if (
|
||||
typeof creationTime !== 'number' ||
|
||||
!Number.isFinite(creationTime) ||
|
||||
creationTime < 0
|
||||
) {
|
||||
return 'renderer-process-metric-creation-time-invalid';
|
||||
}
|
||||
|
||||
const memory = helpers.readRecord(metric['memory']);
|
||||
const workingSetSize = memory?.['workingSetSize'];
|
||||
if (
|
||||
!Number.isSafeInteger(workingSetSize) ||
|
||||
Number(workingSetSize) <= 0 ||
|
||||
Number(workingSetSize) > MAX_WORKING_SET_KIB
|
||||
) {
|
||||
return 'renderer-process-metric-working-set-invalid';
|
||||
}
|
||||
|
||||
return {
|
||||
creationTime,
|
||||
rssBytes: Number(workingSetSize) * KIBIBYTE_BYTES,
|
||||
};
|
||||
},
|
||||
|
||||
unavailableCapture(
|
||||
unavailableReason: RendererProcessRssUnavailableReason,
|
||||
options: {
|
||||
readonly identity?: RendererProcessIdentity | null;
|
||||
readonly missingSampleCount?: number;
|
||||
readonly validSampleCount?: number;
|
||||
} = {}
|
||||
): RendererProcessRssCapture {
|
||||
return Object.freeze({
|
||||
identity: options.identity ?? null,
|
||||
missingSampleCount: options.missingSampleCount ?? 0,
|
||||
peakRssBytes: null,
|
||||
unavailableReason,
|
||||
validSampleCount: options.validSampleCount ?? 0,
|
||||
});
|
||||
},
|
||||
|
||||
invalidateCapture(
|
||||
capture: RendererProcessRssCapture,
|
||||
unavailableReason: RendererProcessRssUnavailableReason,
|
||||
missingSampleCount = capture.missingSampleCount
|
||||
): RendererProcessRssCapture {
|
||||
return helpers.unavailableCapture(unavailableReason, {
|
||||
identity: capture.identity,
|
||||
missingSampleCount,
|
||||
validSampleCount: capture.validSampleCount,
|
||||
});
|
||||
},
|
||||
|
||||
create(
|
||||
webContentsOsPid: unknown,
|
||||
processMetrics: readonly unknown[]
|
||||
): RendererProcessRssCapture {
|
||||
if (!helpers.isValidOsPid(webContentsOsPid)) {
|
||||
return helpers.unavailableCapture('renderer-os-pid-invalid');
|
||||
}
|
||||
|
||||
const candidates = helpers.findPidCandidates(
|
||||
processMetrics,
|
||||
webContentsOsPid
|
||||
);
|
||||
if (candidates.length === 0) {
|
||||
return helpers.unavailableCapture(
|
||||
'renderer-process-metric-missing-at-start',
|
||||
{
|
||||
missingSampleCount: 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (candidates.length !== 1) {
|
||||
return helpers.unavailableCapture(
|
||||
'renderer-process-metric-ambiguous'
|
||||
);
|
||||
}
|
||||
|
||||
const metric = helpers.readMetric(candidates[0] as JsonRecord);
|
||||
if (typeof metric === 'string') {
|
||||
return helpers.unavailableCapture(metric);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
identity: Object.freeze({
|
||||
creationTime: metric.creationTime,
|
||||
pid: webContentsOsPid,
|
||||
}),
|
||||
missingSampleCount: 0,
|
||||
peakRssBytes: metric.rssBytes,
|
||||
unavailableReason: null,
|
||||
validSampleCount: 1,
|
||||
});
|
||||
},
|
||||
|
||||
sample(
|
||||
capture: RendererProcessRssCapture,
|
||||
webContentsOsPid: unknown,
|
||||
processMetrics: readonly unknown[]
|
||||
): RendererProcessRssCapture {
|
||||
if (capture.unavailableReason !== null) {
|
||||
return capture;
|
||||
}
|
||||
if (!helpers.isValidOsPid(webContentsOsPid)) {
|
||||
return helpers.invalidateCapture(
|
||||
capture,
|
||||
'renderer-os-pid-invalid'
|
||||
);
|
||||
}
|
||||
|
||||
const identity = capture.identity;
|
||||
if (identity === null || webContentsOsPid !== identity.pid) {
|
||||
return helpers.invalidateCapture(
|
||||
capture,
|
||||
'renderer-process-identity-changed'
|
||||
);
|
||||
}
|
||||
|
||||
const candidates = helpers.findPidCandidates(
|
||||
processMetrics,
|
||||
webContentsOsPid
|
||||
);
|
||||
if (candidates.length === 0) {
|
||||
return helpers.invalidateCapture(
|
||||
capture,
|
||||
'renderer-process-metric-missing-during-capture',
|
||||
capture.missingSampleCount + 1
|
||||
);
|
||||
}
|
||||
if (candidates.length !== 1) {
|
||||
return helpers.invalidateCapture(
|
||||
capture,
|
||||
'renderer-process-metric-ambiguous'
|
||||
);
|
||||
}
|
||||
|
||||
const metric = helpers.readMetric(candidates[0] as JsonRecord);
|
||||
if (typeof metric === 'string') {
|
||||
return helpers.invalidateCapture(capture, metric);
|
||||
}
|
||||
if (metric.creationTime !== identity.creationTime) {
|
||||
return helpers.invalidateCapture(
|
||||
capture,
|
||||
'renderer-process-identity-changed'
|
||||
);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
identity,
|
||||
missingSampleCount: capture.missingSampleCount,
|
||||
peakRssBytes: Math.max(
|
||||
capture.peakRssBytes ?? metric.rssBytes,
|
||||
metric.rssBytes
|
||||
),
|
||||
unavailableReason: null,
|
||||
validSampleCount: capture.validSampleCount + 1,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unavailableCapture(
|
||||
unavailableReason: RendererProcessRssUnavailableReason,
|
||||
options: {
|
||||
readonly identity?: RendererProcessIdentity | null;
|
||||
readonly missingSampleCount?: number;
|
||||
readonly validSampleCount?: number;
|
||||
} = {}
|
||||
): RendererProcessRssCapture {
|
||||
return Object.freeze({
|
||||
identity: options.identity ?? null,
|
||||
missingSampleCount: options.missingSampleCount ?? 0,
|
||||
peakRssBytes: null,
|
||||
unavailableReason,
|
||||
validSampleCount: options.validSampleCount ?? 0,
|
||||
create: helpers.create,
|
||||
sample: helpers.sample,
|
||||
});
|
||||
}
|
||||
|
||||
function invalidateCapture(
|
||||
capture: RendererProcessRssCapture,
|
||||
unavailableReason: RendererProcessRssUnavailableReason,
|
||||
missingSampleCount = capture.missingSampleCount
|
||||
): RendererProcessRssCapture {
|
||||
return unavailableCapture(unavailableReason, {
|
||||
identity: capture.identity,
|
||||
missingSampleCount,
|
||||
validSampleCount: capture.validSampleCount,
|
||||
});
|
||||
}
|
||||
const rendererProcessRssCaptureApi = createRendererProcessRssCaptureApi();
|
||||
|
||||
export function createRendererProcessRssCapture(
|
||||
webContentsOsPid: unknown,
|
||||
processMetrics: readonly unknown[]
|
||||
): RendererProcessRssCapture {
|
||||
if (!isValidOsPid(webContentsOsPid)) {
|
||||
return unavailableCapture('renderer-os-pid-invalid');
|
||||
}
|
||||
|
||||
const candidates = findPidCandidates(processMetrics, webContentsOsPid);
|
||||
if (candidates.length === 0) {
|
||||
return unavailableCapture('renderer-process-metric-missing-at-start', {
|
||||
missingSampleCount: 1,
|
||||
});
|
||||
}
|
||||
if (candidates.length !== 1) {
|
||||
return unavailableCapture('renderer-process-metric-ambiguous');
|
||||
}
|
||||
|
||||
const metric = readMetric(candidates[0] as JsonRecord);
|
||||
if (typeof metric === 'string') {
|
||||
return unavailableCapture(metric);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
identity: Object.freeze({
|
||||
creationTime: metric.creationTime,
|
||||
pid: webContentsOsPid,
|
||||
}),
|
||||
missingSampleCount: 0,
|
||||
peakRssBytes: metric.rssBytes,
|
||||
unavailableReason: null,
|
||||
validSampleCount: 1,
|
||||
});
|
||||
return rendererProcessRssCaptureApi.create(
|
||||
webContentsOsPid,
|
||||
processMetrics
|
||||
);
|
||||
}
|
||||
|
||||
export function captureRendererProcessRssSample(
|
||||
@@ -154,46 +259,9 @@ export function captureRendererProcessRssSample(
|
||||
webContentsOsPid: unknown,
|
||||
processMetrics: readonly unknown[]
|
||||
): RendererProcessRssCapture {
|
||||
if (capture.unavailableReason !== null) {
|
||||
return capture;
|
||||
}
|
||||
if (!isValidOsPid(webContentsOsPid)) {
|
||||
return invalidateCapture(capture, 'renderer-os-pid-invalid');
|
||||
}
|
||||
|
||||
const identity = capture.identity;
|
||||
if (identity === null || webContentsOsPid !== identity.pid) {
|
||||
return invalidateCapture(capture, 'renderer-process-identity-changed');
|
||||
}
|
||||
|
||||
const candidates = findPidCandidates(processMetrics, webContentsOsPid);
|
||||
if (candidates.length === 0) {
|
||||
return invalidateCapture(
|
||||
capture,
|
||||
'renderer-process-metric-missing-during-capture',
|
||||
capture.missingSampleCount + 1
|
||||
);
|
||||
}
|
||||
if (candidates.length !== 1) {
|
||||
return invalidateCapture(capture, 'renderer-process-metric-ambiguous');
|
||||
}
|
||||
|
||||
const metric = readMetric(candidates[0] as JsonRecord);
|
||||
if (typeof metric === 'string') {
|
||||
return invalidateCapture(capture, metric);
|
||||
}
|
||||
if (metric.creationTime !== identity.creationTime) {
|
||||
return invalidateCapture(capture, 'renderer-process-identity-changed');
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
identity,
|
||||
missingSampleCount: capture.missingSampleCount,
|
||||
peakRssBytes: Math.max(
|
||||
capture.peakRssBytes ?? metric.rssBytes,
|
||||
metric.rssBytes
|
||||
),
|
||||
unavailableReason: null,
|
||||
validSampleCount: capture.validSampleCount + 1,
|
||||
});
|
||||
return rendererProcessRssCaptureApi.sample(
|
||||
capture,
|
||||
webContentsOsPid,
|
||||
processMetrics
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user