@open-assets/open-assets 0.5.0 → 0.5.1
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/README.md +9 -0
- package/lib/render.mjs +36 -4
- package/lib/renderer.mjs +38 -1
- package/package.json +1 -1
- package/skills/open-assets/SKILL.md +3 -1
package/README.md
CHANGED
|
@@ -316,9 +316,18 @@ Options:
|
|
|
316
316
|
| `-o, --output <dir>` | `OPEN_ASSETS_OUTPUT` | `./exports` | Output directory |
|
|
317
317
|
| `--config <path>` | `OPEN_ASSETS_CONFIG` | `assets.json` | Path to config file |
|
|
318
318
|
| `--render-timeout <ms>` | `OPEN_ASSETS_RENDER_TIMEOUT` | `30000` | Puppeteer render timeout |
|
|
319
|
+
| — | `OPEN_ASSETS_CONCURRENCY` | auto | Templates rendered at once (see below) |
|
|
319
320
|
| `--json` | — | — | Output results as JSON |
|
|
320
321
|
| `-q, --quiet` | `OPEN_ASSETS_QUIET` | `false` | Suppress progress logs |
|
|
321
322
|
|
|
323
|
+
**Concurrency**: each template is loaded once per locale and screenshotted at
|
|
324
|
+
every export size from that single page load. A couple of templates render at a
|
|
325
|
+
time, which is where the throughput ceiling sits: Chromium serializes the
|
|
326
|
+
capture path, so extra workers queue behind each other instead of adding speed.
|
|
327
|
+
The default is `min(cores - 1, 2)`. Raise `OPEN_ASSETS_CONCURRENCY` only if your
|
|
328
|
+
templates are unusually slow to lay out relative to how long they take to
|
|
329
|
+
capture, and measure before keeping the change.
|
|
330
|
+
|
|
322
331
|
**Selective export**: flags compose naturally:
|
|
323
332
|
```bash
|
|
324
333
|
# Single template at a single device size
|
package/lib/render.mjs
CHANGED
|
@@ -6,9 +6,41 @@ import { filterCollections, filterTemplates, resolveRenderSizes, flatOutputs } f
|
|
|
6
6
|
import { computeExportChecksum } from "./lockfile.mjs";
|
|
7
7
|
import { loadLocalizations, resolveStrings, getLocales, getDirection } from "./localization.mjs";
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
9
|
+
// Chromium serializes the capture path, so a unit's screenshots cannot overlap
|
|
10
|
+
// each other no matter how many workers ask. A second worker still pays off: it
|
|
11
|
+
// navigates and lays out its page while the first is capturing. A third has
|
|
12
|
+
// nothing left to hide, and each one past that adds a renderer process and a
|
|
13
|
+
// full set of framebuffers while screenshot latency grows in proportion.
|
|
14
|
+
//
|
|
15
|
+
// Measured on a 10-core M-series Mac, 8 templates, median of 5 runs:
|
|
16
|
+
// 4 App Store sizes 1:9.2s 2:9.4s 3:9.7s 4:9.6s 6:9.9s 8:10.4s
|
|
17
|
+
// 1 small size 1:2.0s 2:1.8s 3:2.0s 4:2.0s 6:2.5s 8:3.5s
|
|
18
|
+
const CONCURRENCY_CAP = 2;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pick how many render units run concurrently.
|
|
22
|
+
*
|
|
23
|
+
* The smallest of three ceilings wins: the work actually available, the CPU
|
|
24
|
+
* count (leaving a core for this process and Chromium's browser process), and
|
|
25
|
+
* the cap above. OPEN_ASSETS_CONCURRENCY overrides the result, which is the
|
|
26
|
+
* escape hatch for templates heavy enough to change the arithmetic.
|
|
27
|
+
*
|
|
28
|
+
* @param {unknown[]} units - Render units to be drained by the worker pool
|
|
29
|
+
* @param {{ override?: string|number, cores?: number }} [opts]
|
|
30
|
+
* @returns {number}
|
|
31
|
+
*/
|
|
32
|
+
export function resolveConcurrency(units, opts = {}) {
|
|
33
|
+
if (units.length === 0) return 1;
|
|
34
|
+
|
|
35
|
+
const raw = opts.override ?? process.env.OPEN_ASSETS_CONCURRENCY;
|
|
36
|
+
const override = parseInt(raw, 10);
|
|
37
|
+
if (Number.isFinite(override) && override > 0) {
|
|
38
|
+
return Math.min(override, units.length);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cores = opts.cores ?? cpus().length;
|
|
42
|
+
return Math.max(1, Math.min(units.length, cores - 1, CONCURRENCY_CAP));
|
|
43
|
+
}
|
|
12
44
|
|
|
13
45
|
/**
|
|
14
46
|
* @typedef {object} RenderDeps
|
|
@@ -307,7 +339,7 @@ export async function renderAssets(projectDir, manifest, options, deps, log) {
|
|
|
307
339
|
};
|
|
308
340
|
|
|
309
341
|
const workers = Array.from(
|
|
310
|
-
{ length:
|
|
342
|
+
{ length: resolveConcurrency(units) },
|
|
311
343
|
async () => {
|
|
312
344
|
while (nextUnit < units.length && errors.length === 0) {
|
|
313
345
|
const unit = units[nextUnit++];
|
package/lib/renderer.mjs
CHANGED
|
@@ -45,6 +45,10 @@ export function warmupBrowser() {
|
|
|
45
45
|
return getBrowser();
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// CDP sessions used to keep pooled pages renderable while they sit in the
|
|
49
|
+
// background. Keyed weakly so a closed page's session is not retained.
|
|
50
|
+
const pageSessions = new WeakMap();
|
|
51
|
+
|
|
48
52
|
async function acquirePage() {
|
|
49
53
|
const browser = await getBrowser();
|
|
50
54
|
while (pagePool.length > 0) {
|
|
@@ -52,7 +56,39 @@ async function acquirePage() {
|
|
|
52
56
|
// Drop pages tied to a closed or replaced browser instance
|
|
53
57
|
if (!page.isClosed() && page.browser() === browser) return page;
|
|
54
58
|
}
|
|
55
|
-
|
|
59
|
+
// Every page gets its own browser context, which puts it in its own window.
|
|
60
|
+
// Pages sharing a context are tabs in one window, and headless Chrome only
|
|
61
|
+
// drives the rendering pipeline for the active tab: the rest are frozen, so
|
|
62
|
+
// anything that waits on that pipeline (timers, document.fonts.ready,
|
|
63
|
+
// img.decode()) never settles and the render hangs until protocolTimeout.
|
|
64
|
+
const context = await browser.createBrowserContext();
|
|
65
|
+
const page = await context.newPage();
|
|
66
|
+
// Separate windows still compete for focus, so tell each page to consider
|
|
67
|
+
// itself focused. Without this a page can be treated as occluded and stall
|
|
68
|
+
// the same way a background tab does.
|
|
69
|
+
try {
|
|
70
|
+
const session = await page.createCDPSession();
|
|
71
|
+
await session.send("Emulation.setFocusEmulationEnabled", { enabled: true });
|
|
72
|
+
pageSessions.set(page, session);
|
|
73
|
+
} catch {
|
|
74
|
+
// Best-effort: the browser context already prevents the hard stall.
|
|
75
|
+
}
|
|
76
|
+
return page;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Assert that a page is in the "active" lifecycle state. Chromium may freeze a
|
|
81
|
+
* page that has been idle in the pool; a frozen page runs no timers, so the
|
|
82
|
+
* readiness barrier in settle() would never resolve.
|
|
83
|
+
*/
|
|
84
|
+
async function activatePage(page) {
|
|
85
|
+
const session = pageSessions.get(page);
|
|
86
|
+
if (!session) return;
|
|
87
|
+
try {
|
|
88
|
+
await session.send("Page.setWebLifecycleState", { state: "active" });
|
|
89
|
+
} catch {
|
|
90
|
+
// Best-effort; not all builds implement this on every target.
|
|
91
|
+
}
|
|
56
92
|
}
|
|
57
93
|
|
|
58
94
|
function releasePage(page) {
|
|
@@ -114,6 +150,7 @@ export async function renderVariants(
|
|
|
114
150
|
) {
|
|
115
151
|
const page = await acquirePage();
|
|
116
152
|
try {
|
|
153
|
+
await activatePage(page);
|
|
117
154
|
const first = variants[0];
|
|
118
155
|
await page.setViewport({
|
|
119
156
|
width: first.width,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-assets/open-assets",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Screenshots, icons, and marketing assets, made a breeze in your git repo. Design in HTML/CSS/SVG, render at every size, export anywhere. Like Storybook for marketing assets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -201,6 +201,8 @@ Export entries with a `type` field are post-render actions that run after every
|
|
|
201
201
|
| `--json` | Output results as JSON |
|
|
202
202
|
| `-q, --quiet` | Suppress progress logs |
|
|
203
203
|
|
|
204
|
+
Set `OPEN_ASSETS_CONCURRENCY` to change how many templates render at once. It defaults to `min(cores - 1, 2)`; higher values queue behind Chromium's capture path rather than going faster.
|
|
205
|
+
|
|
204
206
|
Flags compose naturally:
|
|
205
207
|
- `--collection screenshots --template 01-hero --size iphone-6.9` → single template, single size
|
|
206
208
|
- `--collection screenshots --template 01-hero` → one template, all sizes
|
|
@@ -208,7 +210,7 @@ Flags compose naturally:
|
|
|
208
210
|
|
|
209
211
|
#### Performance: prefer targeted renders when iterating
|
|
210
212
|
|
|
211
|
-
A plain `render` is incremental: unchanged templates are skipped via `assets.lock`, so it is usually fast. The renderer also loads each template once per locale and screenshots every export size from that single page load, with
|
|
213
|
+
A plain `render` is incremental: unchanged templates are skipped via `assets.lock`, so it is usually fast. The renderer also loads each template once per locale and screenshots every export size from that single page load, with a couple of templates rendering concurrently. Still, `--force` re-renders **every collection** in `assets.json` (icons, splash, badges, screenshots, etc.), so when you're iterating on a single asset, scope the render:
|
|
212
214
|
|
|
213
215
|
```bash
|
|
214
216
|
# Iterating on one icon (fastest)
|