autokap 1.9.9 → 1.9.10
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/dist/browser-pool.d.ts +17 -0
- package/dist/browser-pool.js +78 -1
- package/dist/browser.js +7 -5
- package/dist/cli-runner.js +9 -1
- package/dist/opcode-runner.js +27 -14
- package/dist/wait-contract.js +4 -1
- package/package.json +1 -1
package/dist/browser-pool.d.ts
CHANGED
|
@@ -2,6 +2,23 @@ import { type BrowserContext } from 'playwright';
|
|
|
2
2
|
import type { BrowserStorageState } from './types.js';
|
|
3
3
|
/** Chromium flags for server-side headless operation (used by pool and standalone launches). */
|
|
4
4
|
export declare const CHROMIUM_ARGS: string[];
|
|
5
|
+
/**
|
|
6
|
+
* Cloud Run NVIDIA L4 GPU launch flags for HEADLESS screenshot capture. Returns
|
|
7
|
+
* null off Cloud Run (Vercel/local have no GPU → keep software rasterization).
|
|
8
|
+
*
|
|
9
|
+
* The clip path (Browser.forClipCapture) proves the L4 binds via ANGLE/Vulkan on
|
|
10
|
+
* this image; screenshots don't need Xvfb (no ffmpeg x11grab), so we use Chromium
|
|
11
|
+
* NEW headless (`--headless=new` — the OLD `--headless` has no GPU) and strip
|
|
12
|
+
* Playwright's default `--headless` so the two don't clash. Verify the binding
|
|
13
|
+
* with the `[gpu-check]` log below: "NVIDIA"/"ANGLE (NVIDIA…)" = hardware,
|
|
14
|
+
* "SwiftShader"/"llvmpipe" = it fell back to software (then we'd need Xvfb).
|
|
15
|
+
* The GPU accelerates page render/composite (the win for animated/blur-heavy
|
|
16
|
+
* pages); the final PNG encode stays CPU-bound.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveCloudGpuLaunch(): {
|
|
19
|
+
args: string[];
|
|
20
|
+
ignoreDefaultArgs: string[];
|
|
21
|
+
} | null;
|
|
5
22
|
declare class BrowserPool {
|
|
6
23
|
private browser;
|
|
7
24
|
private launchPromise;
|
package/dist/browser-pool.js
CHANGED
|
@@ -54,6 +54,66 @@ const MAX_CAPTURES_BEFORE_RESTART = 100;
|
|
|
54
54
|
* for recovery instead of eating the whole variant. See usage in acquireContext.
|
|
55
55
|
*/
|
|
56
56
|
const POOL_CONTEXT_ACTION_TIMEOUT_MS = 25000;
|
|
57
|
+
/**
|
|
58
|
+
* Cloud Run NVIDIA L4 GPU launch flags for HEADLESS screenshot capture. Returns
|
|
59
|
+
* null off Cloud Run (Vercel/local have no GPU → keep software rasterization).
|
|
60
|
+
*
|
|
61
|
+
* The clip path (Browser.forClipCapture) proves the L4 binds via ANGLE/Vulkan on
|
|
62
|
+
* this image; screenshots don't need Xvfb (no ffmpeg x11grab), so we use Chromium
|
|
63
|
+
* NEW headless (`--headless=new` — the OLD `--headless` has no GPU) and strip
|
|
64
|
+
* Playwright's default `--headless` so the two don't clash. Verify the binding
|
|
65
|
+
* with the `[gpu-check]` log below: "NVIDIA"/"ANGLE (NVIDIA…)" = hardware,
|
|
66
|
+
* "SwiftShader"/"llvmpipe" = it fell back to software (then we'd need Xvfb).
|
|
67
|
+
* The GPU accelerates page render/composite (the win for animated/blur-heavy
|
|
68
|
+
* pages); the final PNG encode stays CPU-bound.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveCloudGpuLaunch() {
|
|
71
|
+
const isCloudRunner = process.env.AUTOKAP_CLOUD_RUNNER === '1';
|
|
72
|
+
if (process.platform !== 'linux' || !isCloudRunner)
|
|
73
|
+
return null;
|
|
74
|
+
return {
|
|
75
|
+
args: [
|
|
76
|
+
'--headless=new',
|
|
77
|
+
'--use-gl=angle',
|
|
78
|
+
'--use-angle=vulkan',
|
|
79
|
+
'--enable-features=Vulkan',
|
|
80
|
+
'--ignore-gpu-blocklist',
|
|
81
|
+
'--enable-gpu-rasterization',
|
|
82
|
+
'--enable-zero-copy',
|
|
83
|
+
],
|
|
84
|
+
ignoreDefaultArgs: ['--headless'],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Log the active WebGL renderer once per pooled browser launch so a Cloud Run
|
|
88
|
+
* deploy can confirm the L4 bound (NVIDIA) vs a silent SwiftShader fallback. */
|
|
89
|
+
async function logPoolGpuRenderer(browser) {
|
|
90
|
+
try {
|
|
91
|
+
const context = await browser.newContext();
|
|
92
|
+
try {
|
|
93
|
+
const page = await context.newPage();
|
|
94
|
+
const info = await page.evaluate(() => {
|
|
95
|
+
const canvas = document.createElement('canvas');
|
|
96
|
+
const gl = (canvas.getContext('webgl2') || canvas.getContext('webgl'));
|
|
97
|
+
if (!gl)
|
|
98
|
+
return { error: 'no webgl context' };
|
|
99
|
+
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
|
100
|
+
if (!ext)
|
|
101
|
+
return { error: 'no debug_renderer_info extension' };
|
|
102
|
+
return {
|
|
103
|
+
vendor: gl.getParameter(ext.UNMASKED_VENDOR_WEBGL),
|
|
104
|
+
renderer: gl.getParameter(ext.UNMASKED_RENDERER_WEBGL),
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
console.info('[gpu-check] pool WebGL renderer:', JSON.stringify(info));
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
await context.close();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
console.warn('[gpu-check] pool WebGL query failed:', err.message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
57
117
|
function normalizeDeviceScaleFactor(value) {
|
|
58
118
|
if (!Number.isFinite(value))
|
|
59
119
|
return 2;
|
|
@@ -134,7 +194,24 @@ class BrowserPool {
|
|
|
134
194
|
await this.launchPromise;
|
|
135
195
|
}
|
|
136
196
|
async launchBrowser() {
|
|
137
|
-
|
|
197
|
+
const gpu = resolveCloudGpuLaunch();
|
|
198
|
+
if (gpu) {
|
|
199
|
+
// Cloud Run L4: drop the software-only flags and enable Vulkan/ANGLE GPU
|
|
200
|
+
// rasterization for headless screenshot capture, mirroring the clip path.
|
|
201
|
+
const args = [
|
|
202
|
+
...CHROMIUM_ARGS.filter((a) => a !== '--disable-gpu' && a !== '--disable-gpu-sandbox'),
|
|
203
|
+
...gpu.args,
|
|
204
|
+
];
|
|
205
|
+
this.browser = await chromium.launch({
|
|
206
|
+
headless: true,
|
|
207
|
+
ignoreDefaultArgs: gpu.ignoreDefaultArgs,
|
|
208
|
+
args,
|
|
209
|
+
});
|
|
210
|
+
await logPoolGpuRenderer(this.browser);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
this.browser = await chromium.launch({ headless: true, args: CHROMIUM_ARGS });
|
|
214
|
+
}
|
|
138
215
|
this.captureCount = 0;
|
|
139
216
|
this.browser.on('disconnected', () => {
|
|
140
217
|
// Browser crashed — active contexts are now dead, they'll throw naturally
|
package/dist/browser.js
CHANGED
|
@@ -222,12 +222,14 @@ async function seedCloudChromiumPreferences(userDataDir) {
|
|
|
222
222
|
*/
|
|
223
223
|
const CONTEXT_ACTION_TIMEOUT_MS = 25000;
|
|
224
224
|
/**
|
|
225
|
-
* Explicit
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
225
|
+
* Explicit backstop for the heavy screenshot primitives. Sits just under the 45s
|
|
226
|
+
* global screenshot deadline and ABOVE the runner's reserved action budget
|
|
227
|
+
* (~global − recovery reserve), so the runner's per-attempt bound is the real
|
|
228
|
+
* control and this only catches a screenshot that has genuinely wedged. Must not
|
|
229
|
+
* be smaller than a legitimate slow high-res capture, or it would pre-empt the
|
|
230
|
+
* runner and fail captures that would have completed.
|
|
229
231
|
*/
|
|
230
|
-
const SCREENSHOT_PRIMITIVE_TIMEOUT_MS =
|
|
232
|
+
const SCREENSHOT_PRIMITIVE_TIMEOUT_MS = 40000;
|
|
231
233
|
/** Apply our engine-wide Playwright defaults to a freshly created context. */
|
|
232
234
|
function applyContextDefaults(context) {
|
|
233
235
|
context.setDefaultTimeout(CONTEXT_ACTION_TIMEOUT_MS);
|
package/dist/cli-runner.js
CHANGED
|
@@ -209,7 +209,15 @@ export async function runCapture(options) {
|
|
|
209
209
|
// serialize variants (CPU/RAM bound on the encoder side). Only 'screenshot'
|
|
210
210
|
// honors the requested concurrency.
|
|
211
211
|
const isRecordable = program.mediaMode === 'clip' || program.mediaMode === 'video';
|
|
212
|
-
|
|
212
|
+
// AUTOKAP_MAX_PARALLEL_CAPTURES lets the RUNTIME (e.g. the Cloud Run container)
|
|
213
|
+
// clamp per-run variant concurrency below what the server compiled, decoupled
|
|
214
|
+
// from billing/queue limits. Fewer concurrent captures = more CPU/GPU per
|
|
215
|
+
// capture on a shared L4 container. Unset/invalid → honor the compiled value.
|
|
216
|
+
const runtimeConcurrencyCap = Number.parseInt(process.env.AUTOKAP_MAX_PARALLEL_CAPTURES ?? '', 10);
|
|
217
|
+
const requestedConcurrency = Number.isFinite(runtimeConcurrencyCap) && runtimeConcurrencyCap > 0
|
|
218
|
+
? Math.min(program.maxParallelCaptures ?? runtimeConcurrencyCap, runtimeConcurrencyCap)
|
|
219
|
+
: program.maxParallelCaptures;
|
|
220
|
+
const maxParallelVariants = isRecordable ? 1 : requestedConcurrency;
|
|
213
221
|
// AUT-149: collect structured logs + progress events for export on failure.
|
|
214
222
|
const logCollector = new LogCollector();
|
|
215
223
|
logCollector.start();
|
package/dist/opcode-runner.js
CHANGED
|
@@ -56,13 +56,18 @@ export class NoOpRecoveryChain {
|
|
|
56
56
|
const MIN_CLIP_FINALIZATION_TIMEOUT_MS = 30000;
|
|
57
57
|
const DEFAULT_VIDEO_RECORDING_RESOLUTION = { width: 1920, height: 1080 };
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
59
|
+
* Recovery budget reserved from the global capture deadline. The screenshot
|
|
60
|
+
* buffer (the artifact-critical call) may use the REST of the deadline, so a
|
|
61
|
+
* slow-but-completing high-res capture (e.g. a 5K device frame legitimately
|
|
62
|
+
* needing 15-30s under load) succeeds, while a truly hung one still fails with
|
|
63
|
+
* this slice left for the recovery/recapture path. Replaces the old flat 12s
|
|
64
|
+
* buffer cap, which was smaller than legitimate heavy captures and killed them
|
|
65
|
+
* (and killed the recapture too, since it re-applied the same 12s wall).
|
|
64
66
|
*/
|
|
65
|
-
const
|
|
67
|
+
const CAPTURE_RECOVERY_RESERVE_MS = 10000;
|
|
68
|
+
/** Floor for the first-attempt capture budget when little global time remains. */
|
|
69
|
+
const MIN_CAPTURE_ACTION_BUDGET_MS = 8000;
|
|
70
|
+
/** Best-effort bound for browser-mockup metadata (URL / favicon / title). */
|
|
66
71
|
const CAPTURE_METADATA_TIMEOUT_MS = 3000;
|
|
67
72
|
/**
|
|
68
73
|
* The compiled per-opcode action budget. For CAPTURE_SCREENSHOT this governs
|
|
@@ -510,7 +515,15 @@ async function executeOpcode(opcode, index, adapter, verifier, breaker, recovery
|
|
|
510
515
|
const isPureWait = opcode.kind === 'WAIT_FOR';
|
|
511
516
|
const usesGlobalDeadline = isPureWait || isArtifactProducingOpcode(opcode.kind);
|
|
512
517
|
const actionDeadlineMs = usesGlobalDeadline ? globalDeadlineMs : deadlineMs;
|
|
513
|
-
|
|
518
|
+
let actionBudgetMs = getRemainingTimeMs(actionDeadlineMs);
|
|
519
|
+
// Reserve a slice of the global deadline for recovery/recapture on the
|
|
520
|
+
// artifact-critical CAPTURE_SCREENSHOT: the capture uses the rest (so a slow
|
|
521
|
+
// high-res shot completes) yet a hung one still leaves room to recover.
|
|
522
|
+
// END_CLIP keeps its full budget — its finalization hard-fails without a
|
|
523
|
+
// recapture, so reserving would only shorten it.
|
|
524
|
+
if (opcode.kind === 'CAPTURE_SCREENSHOT') {
|
|
525
|
+
actionBudgetMs = Math.max(MIN_CAPTURE_ACTION_BUDGET_MS, actionBudgetMs - CAPTURE_RECOVERY_RESERVE_MS);
|
|
526
|
+
}
|
|
514
527
|
if (actionBudgetMs <= 0) {
|
|
515
528
|
const reason = `timeout after ${effectiveTimeoutMs}ms`;
|
|
516
529
|
logger.debug(`[opcode ${index}] no budget left after captureBeforeState (deadline=${actionDeadlineMs}, now=${Date.now()})`);
|
|
@@ -891,13 +904,13 @@ async function executeOpcodeAction(opcode, opcodeIndex, adapter, artifacts, tele
|
|
|
891
904
|
lowConfidenceReasons.push(`captured before visual stability: ${stability.reason}`);
|
|
892
905
|
}
|
|
893
906
|
const captureLowConfidenceReason = lowConfidenceReasons.join('; ') || undefined;
|
|
894
|
-
// Capture the screenshot buffer FIRST
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
// recovery
|
|
900
|
-
const buffer = await
|
|
907
|
+
// Capture the screenshot buffer FIRST — the only artifact-critical call.
|
|
908
|
+
// It is bounded by the reserved action budget (the withTimeout around this
|
|
909
|
+
// whole action in executeOpcode) and the screenshot primitive's own
|
|
910
|
+
// timeout — NOT a flat sub-cap — so a legitimately slow high-res capture
|
|
911
|
+
// can use most of the deadline while a hung one still frees the reserved
|
|
912
|
+
// recovery slice (handleFailure → recapture in `failWithRecovery`).
|
|
913
|
+
const buffer = await takeCaptureBuffer(adapter, opcode);
|
|
901
914
|
// Everything below is best-effort browser-mockup metadata. The buffer is
|
|
902
915
|
// already in hand, so a slow/hung metadata call must NEVER throw out of
|
|
903
916
|
// the action and discard a good screenshot. Each is individually bounded
|
package/dist/wait-contract.js
CHANGED
|
@@ -23,7 +23,10 @@ export const WAIT_CONTRACT_VERSION = 1;
|
|
|
23
23
|
* so it never shortens an intentionally-long opcode (`SLEEP`, `END_CLIP`).
|
|
24
24
|
*/
|
|
25
25
|
export const GLOBAL_WAIT_CAP_MS = {
|
|
26
|
-
|
|
26
|
+
// Heavy high-res captures (5K device frames, retina, animated pages) can take
|
|
27
|
+
// 15-30s under cloud CPU load with software rasterization. 45s gives them room
|
|
28
|
+
// to complete (and to reserve a recovery slice) instead of failing prematurely.
|
|
29
|
+
screenshot: 45_000,
|
|
27
30
|
clip: 45_000,
|
|
28
31
|
video: 60_000,
|
|
29
32
|
};
|