autokap 1.9.9 → 2.0.0

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.
@@ -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;
@@ -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
- this.browser = await chromium.launch({ headless: true, args: CHROMIUM_ARGS });
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 ceiling for the heavy screenshot primitives. Below the context
226
- * default so an already-abandoned capture (the runner gives up earlier, ~12s)
227
- * stops burning renderer CPU quickly instead of contending with sibling
228
- * variants until the 25s default fires.
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 = 20000;
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);
@@ -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
- const maxParallelVariants = isRecordable ? 1 : program.maxParallelCaptures;
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();