@roku-sdk/ai 0.0.0-stage → 2.19.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -2
  3. package/package.json +47 -3
  4. package/skills/ads-and-raf/SKILL.md +231 -0
  5. package/skills/ads-and-raf/references/integration-flows.md +332 -0
  6. package/skills/app-packaging/SKILL.md +107 -0
  7. package/skills/app-performance/SKILL.md +133 -0
  8. package/skills/cli-dev-loop/SKILL.md +217 -0
  9. package/skills/cli-dev-loop/references/logs-and-crashes.md +114 -0
  10. package/skills/cpu-and-heap-profiling/SKILL.md +108 -0
  11. package/skills/cpu-and-heap-profiling/references/reading-profiles.md +52 -0
  12. package/skills/data-entry-keyboards/SKILL.md +160 -0
  13. package/skills/device-management/SKILL.md +158 -0
  14. package/skills/dialogs-and-modals/SKILL.md +210 -0
  15. package/skills/effects/SKILL.md +183 -0
  16. package/skills/fetch-and-data-loading/SKILL.md +164 -0
  17. package/skills/fetch-and-data-loading/references/https-and-certificates.md +79 -0
  18. package/skills/flex-grid/SKILL.md +236 -0
  19. package/skills/flex-grid/references/focus-integration.md +355 -0
  20. package/skills/flex-grid/references/plugins.md +201 -0
  21. package/skills/focus-management/SKILL.md +210 -0
  22. package/skills/focus-management/references/focus-recipes.md +290 -0
  23. package/skills/fonts-and-glyphs/SKILL.md +231 -0
  24. package/skills/fonts-and-glyphs/references/glyph-tables.md +128 -0
  25. package/skills/logging-and-errors/SKILL.md +227 -0
  26. package/skills/logging-and-errors/references/uncaught-exceptions.md +137 -0
  27. package/skills/mcp-ai-dev-loop/SKILL.md +189 -0
  28. package/skills/mcp-ai-dev-loop/references/crash-and-console.md +68 -0
  29. package/skills/mcp-ai-dev-loop/references/live-debugging.md +129 -0
  30. package/skills/mcp-setup/SKILL.md +131 -0
  31. package/skills/project-setup/SKILL.md +210 -0
  32. package/skills/project-setup/references/roku-config.md +120 -0
  33. package/skills/screen-navigation/SKILL.md +166 -0
  34. package/skills/sdk-components/SKILL.md +213 -0
  35. package/skills/sdk-components/references/dialogs.md +148 -0
  36. package/skills/sdk-components/references/scenegraph-node-spelling.md +123 -0
  37. package/skills/sdk-installation/SKILL.md +129 -0
  38. package/skills/slideshow/SKILL.md +157 -0
  39. package/skills/slideshow/references/focus-and-handoff.md +187 -0
  40. package/skills/solid-on-roku/SKILL.md +181 -0
  41. package/skills/solid-on-roku/references/hermes-and-exports.md +130 -0
  42. package/skills/testing/SKILL.md +147 -0
  43. package/skills/testing/references/async-and-timing.md +97 -0
  44. package/skills/user-accounts-and-purchases/SKILL.md +164 -0
  45. package/skills-manifest.json +316 -0
  46. package/skills-manifest.schema.json +62 -0
  47. package/src/__tests__/manifest.test.ts +267 -0
  48. package/src/__tests__/snippets.test.ts +221 -0
  49. package/src/__tests__/trigger-routing.test.ts +77 -0
  50. package/src/index.ts +36 -0
  51. package/src/manifest.ts +215 -0
  52. package/src/router.ts +147 -0
  53. package/src/snippets.ts +212 -0
@@ -0,0 +1,332 @@
1
+ # Ads integration flows: CSAI, SSAI and CSAS
2
+
3
+ One complete wired module per mode. All three obey the same protocol — wait for `"ready"`, set the
4
+ data fields then `command`, read the matching callback — and differ only in which commands they
5
+ issue and which nodes they need.
6
+
7
+ ## What differs between the three
8
+
9
+ | | CSAI | SSAI | CSAS |
10
+ |---|---|---|---|
11
+ | Command sequence | `getAds` → `showAds`, per break | `getAds` → `stitchedAdsInit` | `getAds` → `constructStitchedStream` → `renderStitchedStream` |
12
+ | `adView` | required | not used | required |
13
+ | `contentView` | required | required | not used |
14
+ | `contentMetadata` | not used | not used | required |
15
+ | Who owns the `Video` | the app | the app | RAF — the app renders no `Video` at all |
16
+ | Result callbacks | `onGetAdsResult`, `onShowAdsResult` | `onGetAdsResult`, `onStitchedAdResult` | `onGetAdsResult`, `onConstructStitchedStreamResult`, `onRenderStitchedStreamResult` |
17
+
18
+ The `Video`-ownership row is the one that surprises people. CSAS hands RAF a single stream that
19
+ already has the ads spliced in, and RAF plays it; there is no content `Video` in the tree and no
20
+ `contentView` to give, so nothing in a CSAS module reacts to playback position.
21
+
22
+ ## CSAI: pause the content, play the break, resume
23
+
24
+ The break's resume point has to be captured **before** the content is stopped, because stopping it
25
+ is what discards the position. `position` is milliseconds and `ContentNode`'s `playStart` is
26
+ seconds. `-1` is the "not started" sentinel: 0 ms is a real cue point, so 0 cannot be it.
27
+
28
+ Mid-roll arrives as another `onGetAdsResult`, from a `getAds` the task fired itself. That is why
29
+ the same handler serves pre-roll and every mid-roll, and why the empty-pods branch is guarded on
30
+ `position() === -1` — an empty mid-roll result means "no fill for this break", and restarting
31
+ content on it would fight the playback already running.
32
+
33
+ ```tsx rsg-skill=complete
34
+ import type { AdsCommand, AdsState, EventHandler, EventHandlerAdPods, ShowAdsResult } from "@roku-sdk/ads";
35
+ import { Ads } from "@roku-sdk/ads";
36
+ import { Component, ContentNode, Group, MediaControl, Rectangle, Video } from "@roku-sdk/components";
37
+ import { DomNode } from "@roku-sdk/interfaces";
38
+ import { createSignal } from "@roku-sdk/solid";
39
+
40
+ const AD_TAG = "https://example.com/tag.vmap";
41
+ const CONTENT_URL = "https://example.com/content.m3u8";
42
+
43
+ export const CsaiPlayer: Component = () => {
44
+ const [command, setCommand] = createSignal<AdsCommand>("none");
45
+ const [adUrl, setAdUrl] = createSignal("");
46
+ const [adView, setAdView] = createSignal<DomNode | undefined>();
47
+ const [contentView, setContentView] = createSignal<DomNode | undefined>();
48
+ const [control, setControl] = createSignal<MediaControl>("none");
49
+ const [position, setPosition] = createSignal(-1);
50
+ const [playStart, setPlayStart] = createSignal(0);
51
+
52
+ const handleAdsState = (state: AdsState) => {
53
+ if (state !== "ready") return;
54
+ setAdUrl(AD_TAG);
55
+ setCommand("getAds");
56
+ };
57
+
58
+ const handleGetAdsResult: EventHandlerAdPods = pods => {
59
+ if (pods.length > 0) {
60
+ setPlayStart(position() === -1 ? 0 : position() / 1000);
61
+ setControl("stop");
62
+ setCommand("showAds");
63
+ } else if (position() === -1) {
64
+ setControl("play");
65
+ }
66
+ };
67
+
68
+ const handleShowAdsResult: EventHandler<ShowAdsResult> = result => {
69
+ if (result === "none") return;
70
+ if (result === "completed") {
71
+ setControl("play");
72
+ setCommand("none");
73
+ } else {
74
+ setControl("stop");
75
+ setCommand("stop");
76
+ }
77
+ };
78
+
79
+ return (
80
+ <group>
81
+ <Video
82
+ width={1920}
83
+ height={1080}
84
+ content={<contentNode url={CONTENT_URL} playStart={playStart()} />}
85
+ control={control()}
86
+ notificationInterval={1.0}
87
+ onPosition={value => setPosition(value)}
88
+ ref={element => setContentView(element)}
89
+ gainFocus={control() === "play"}
90
+ />
91
+ <rectangle
92
+ width={1920}
93
+ height={1080}
94
+ color="0x000000FF"
95
+ visible={control() !== "play"}
96
+ ref={element => setAdView(element)}
97
+ />
98
+ <Ads
99
+ adUrl={adUrl()}
100
+ adView={adView()}
101
+ contentView={contentView()}
102
+ command={command()}
103
+ onAdsState={handleAdsState}
104
+ onGetAdsResult={handleGetAdsResult}
105
+ onShowAdsResult={handleShowAdsResult}
106
+ />
107
+ </group>
108
+ );
109
+ };
110
+ ```
111
+
112
+ `gainFocus={control() === "play"}` is doing more than it looks. An ad break takes focus and does
113
+ not give it back, so without a re-assertion on resume the app plays content that answers no remote
114
+ keys. Tying it to the resume condition is the cheapest correct place for it.
115
+
116
+ ## SSAI: the stream already contains the ads
117
+
118
+ The app plays one stream and asks RAF what is on screen. `getAds` here exists only to give
119
+ `stitchedAdsInit` its pods — nothing is rendered from the result.
120
+
121
+ There is a **race worth respecting**: `stitchedAdsInit` has to reach the task before position
122
+ events start arriving, because the task is still in CSAI mode until it lands, and a position event
123
+ that arrives first is treated as a CSAI cue point. Start playback from
124
+ `onStitchedAdsInitResult` rather than from `onGetAdsResult`, and the race is answered by the
125
+ protocol instead of by a timing margin. Only `"completed"` may start playback: an `"error"` means
126
+ the mode never changed, so playing anyway puts position events back on the CSAI cue-point path
127
+ against a stream that was never initialised.
128
+
129
+ `onStitchedAdResult` receives RAF's own associative array, so its keys are lowercase:
130
+ `evthandled`, not `evtHandled`. That is the general rule for anything RAF returns.
131
+
132
+ The module below needs **2.15.0 or newer**: that is the release `onStitchedAdsInitResult` and its
133
+ `StitchedAdsInitResult` type arrived in. The `stitchedAdsInit` command itself is older, but nothing
134
+ acknowledged it before then, so on 2.14.0 the only place to start playback from is
135
+ `onGetAdsResult` — which is the race above, answerable there by a timing margin and nothing better.
136
+
137
+ ```tsx rsg-skill=complete
138
+ import type {
139
+ AdsCommand,
140
+ AdsState,
141
+ EventHandler,
142
+ EventHandlerAdPods,
143
+ StitchedAdsInitResult
144
+ } from "@roku-sdk/ads";
145
+ import { Ads } from "@roku-sdk/ads";
146
+ import { Component, ContentNode, Group, MediaControl, Video } from "@roku-sdk/components";
147
+ import { DomNode } from "@roku-sdk/interfaces";
148
+ import { createSignal } from "@roku-sdk/solid";
149
+
150
+ const AD_TAG = "https://example.com/tag.vmap";
151
+ const STITCHED_URL = "https://example.com/stitched.m3u8";
152
+
153
+ export const SsaiPlayer: Component = () => {
154
+ const [command, setCommand] = createSignal<AdsCommand>("none");
155
+ const [adUrl, setAdUrl] = createSignal("");
156
+ const [contentView, setContentView] = createSignal<DomNode | undefined>();
157
+ const [control, setControl] = createSignal<MediaControl>("none");
158
+ const [adPlaying, setAdPlaying] = createSignal(false);
159
+
160
+ const handleAdsState = (state: AdsState) => {
161
+ if (state !== "ready") return;
162
+ setAdUrl(AD_TAG);
163
+ setCommand("getAds");
164
+ };
165
+
166
+ const handleGetAdsResult: EventHandlerAdPods = pods => {
167
+ if (pods.length === 0) {
168
+ setControl("play");
169
+ return;
170
+ }
171
+ setCommand("stitchedAdsInit");
172
+ };
173
+
174
+ // Playback starts here, once the task confirms it is in SSAI mode.
175
+ const handleStitchedAdsInitResult: EventHandler<StitchedAdsInitResult> = result => {
176
+ if (result === "none") return;
177
+ if (result === "error") {
178
+ // Still in CSAI: position events would drive mid-roll against a stream never
179
+ // set up for it, so this is terminal rather than a warning to play past.
180
+ setCommand("stop");
181
+ return;
182
+ }
183
+ setControl("play");
184
+ };
185
+
186
+ const handleStitchedAdResult: EventHandler<Record<string, unknown>> = result => {
187
+ setAdPlaying(result?.evthandled === true);
188
+ };
189
+
190
+ return (
191
+ <group>
192
+ <Video
193
+ width={1920}
194
+ height={1080}
195
+ content={<contentNode url={STITCHED_URL} />}
196
+ control={control()}
197
+ notificationInterval={1.0}
198
+ ref={element => setContentView(element)}
199
+ gainFocus={control() === "play" && !adPlaying()}
200
+ />
201
+ <Ads
202
+ adUrl={adUrl()}
203
+ contentView={contentView()}
204
+ command={command()}
205
+ onAdsState={handleAdsState}
206
+ onGetAdsResult={handleGetAdsResult}
207
+ onStitchedAdsInitResult={handleStitchedAdsInitResult}
208
+ onStitchedAdResult={handleStitchedAdResult}
209
+ />
210
+ </group>
211
+ );
212
+ };
213
+ ```
214
+
215
+ `stitchedAdsInit` on empty pods changes no mode and answers `"error"`. The `pods.length === 0`
216
+ branch above is what keeps that case from arising at all; the result is the backstop, and reading
217
+ it as anything other than terminal leaves the app playing a stitched stream through a task still
218
+ in CSAI.
219
+
220
+ ## CSAS: build one stream, then render it
221
+
222
+ Two commands in sequence, each gated on the previous one's result. `contentMetadata` describes the
223
+ content to be stitched: a `ContentNode` prop bag, so `url` and `length` in seconds.
224
+
225
+ The task writes `"none"` to both result fields before it writes the real value, so guard on it
226
+ before branching. The shape to avoid is `if (result === "completed") { … } else { fail }`: it puts
227
+ the sentinel down the failure path, which here means abandoning the session before RAF has
228
+ answered.
229
+
230
+ A failed `constructStitchedStream` puts the task back in CSAI mode. Its `"error"` result is the
231
+ only notice of that, so treat it as terminal for the session rather than retrying
232
+ `renderStitchedStream`.
233
+
234
+ ```tsx rsg-skill=complete
235
+ import type {
236
+ AdsCommand,
237
+ AdsState,
238
+ ConstructStitchedStreamResult,
239
+ EventHandler,
240
+ EventHandlerAdPods,
241
+ RenderStitchedStreamResult
242
+ } from "@roku-sdk/ads";
243
+ import { Ads } from "@roku-sdk/ads";
244
+ import { Component, Group, IContentNodeProps, Rectangle } from "@roku-sdk/components";
245
+ import { DomNode } from "@roku-sdk/interfaces";
246
+ import { createSignal } from "@roku-sdk/solid";
247
+
248
+ const AD_TAG = "https://example.com/tag.vmap";
249
+
250
+ export const CsasPlayer: Component<{ onFinished: () => void }> = props => {
251
+ const [command, setCommand] = createSignal<AdsCommand>("none");
252
+ const [adUrl, setAdUrl] = createSignal("");
253
+ const [adView, setAdView] = createSignal<DomNode | undefined>();
254
+ const [contentMetadata, setContentMetadata] = createSignal<IContentNodeProps>();
255
+ const [playing, setPlaying] = createSignal(false);
256
+
257
+ const handleAdsState = (state: AdsState) => {
258
+ if (state !== "ready") return;
259
+ setContentMetadata({ url: "https://example.com/content.m3u8", length: 600 });
260
+ // Measurement fields must be in place before the pods are fetched.
261
+ setAdUrl(AD_TAG);
262
+ setPlaying(true);
263
+ setCommand("getAds");
264
+ };
265
+
266
+ const handleGetAdsResult: EventHandlerAdPods = () => {
267
+ setCommand("constructStitchedStream");
268
+ };
269
+
270
+ const handleConstructResult: EventHandler<ConstructStitchedStreamResult> = result => {
271
+ if (result === "none") return;
272
+ if (result === "completed") {
273
+ setCommand("renderStitchedStream");
274
+ } else {
275
+ setPlaying(false);
276
+ props.onFinished();
277
+ }
278
+ };
279
+
280
+ const handleRenderResult: EventHandler<RenderStitchedStreamResult> = result => {
281
+ if (result === "none") return;
282
+ // "user_exited" is a viewer back-out, not a failure.
283
+ setPlaying(false);
284
+ setCommand("none");
285
+ props.onFinished();
286
+ };
287
+
288
+ return (
289
+ <group>
290
+ <rectangle
291
+ width={1920}
292
+ height={1080}
293
+ color="0x000000FF"
294
+ visible={playing()}
295
+ gainFocus={playing()}
296
+ ref={element => setAdView(element)}
297
+ />
298
+ <Ads
299
+ adUrl={adUrl()}
300
+ adView={adView()}
301
+ contentMetadata={contentMetadata()}
302
+ command={command()}
303
+ enableAdMeasurements={true}
304
+ contentGenre="Entertainment"
305
+ contentId="csas-sample"
306
+ onAdsState={handleAdsState}
307
+ onGetAdsResult={handleGetAdsResult}
308
+ onConstructStitchedStreamResult={handleConstructResult}
309
+ onRenderStitchedStreamResult={handleRenderResult}
310
+ />
311
+ </group>
312
+ );
313
+ };
314
+ ```
315
+
316
+ `onGetAdsResult` ignores its pods here on purpose: `constructStitchedStream` reads the ads the task
317
+ already holds, so there is nothing for the app to pass along and no reason to inspect them.
318
+
319
+ ## Debug output
320
+
321
+ `debugOutput={true}` turns on RAF's own logging, which is the only view into what the library did
322
+ with a tag it rejected. It is verbose and belongs in development only — set it from the same
323
+ `"ready"` branch that sets `adUrl`, and leave it off in anything you ship.
324
+
325
+ ## Stopping
326
+
327
+ `command = "stop"` ends the task's loop and reports `"stopped"` on `onAdsState`. Issue it while the
328
+ component is still mounted — from a Back key handler, or wherever the app decides to leave the
329
+ player. Doing it from `onCleanup` alone is best-effort: SolidJS may already have disposed the
330
+ effect that writes the field, and `onAdsState` is gone either way, so nothing confirms it. The task
331
+ does self-terminate when SceneGraph destroys the node, so the cleanup is a reasonable belt-and-
332
+ braces addition — just not the primary path.
@@ -0,0 +1,107 @@
1
+ # App Packaging
2
+
3
+ ## Overview
4
+
5
+ `rk device package` and `rk dcl` talk to a Roku over its developer interface: they sign using the key
6
+ already on that box and manage what it holds, so they need a device. Signing is where the SDK
7
+ stops. Getting a version onto a channel, staged for review or live, is done by uploading the
8
+ signed `.pkg` in the Roku Developer Dashboard; the SDK has no command for it.
9
+
10
+ ## When to Use
11
+
12
+ - producing a signed `.pkg` from an app that is already installed on a device
13
+ - a signing step in CI, where nothing can be typed at a prompt
14
+ - pointing a box at a different signing key, or checking which developer ID it now reports
15
+ - getting a version onto a channel, staged for review or published outright, and which part of
16
+ that the SDK does
17
+ - listing or clearing the dev channel and the Dynamic Component Libraries a device holds
18
+ - a partner shipping a library separately for the app to load from a URL at runtime, and where
19
+ setting that library up and referencing a node from it is written down
20
+
21
+ ### When not to use
22
+
23
+ | Situation | Use instead | Because |
24
+ |---|---|---|
25
+ | Installing `@roku-sdk` packages from npmjs.com | The `sdk-installation` skill | That is what arrives; this is what leaves |
26
+ | Cutting a release of the SDK's own packages | The repository's release process | This ships an app to a channel, not a library to a registry |
27
+ | Producing the build that then gets signed | The `cli-dev-loop` skill | Signing starts from an app that already exists on the box |
28
+ | Writing the code that builds a Dynamic Component Library, or loads one and pulls a node out of it | `documentation/core-concepts/dcl.md` | No other skill covers it, so the two Quick Reference rows below are the whole routing answer: `rk dcl` manages the ones a device already holds, and building one is an app whose `roku-config.ts` sets `app.type: "dcl"`, side-loaded like any other |
29
+ | What `roku-config.ts` declares about the app | The `project-setup` skill | Its `package` block is read here, but the file belongs there |
30
+ | Reaching the device at all, or which one a command lands on | The `device-management` skill | Everything here takes `--device` or `--host`; what those resolve to is decided there |
31
+
32
+ ## Quick Reference
33
+
34
+ | Goal | Do |
35
+ |---|---|
36
+ | Sign what is installed on the device | `rk device package sign` |
37
+ | Sign with nothing to type at | `ROKU_SIGNING_PASSWORD` in the environment |
38
+ | Point a box at another signing key | `rk device package rekey <pkg-path>` |
39
+ | See what is installed | `rk device package ls` |
40
+ | Remove the dev channel | `rk device package delete` |
41
+ | Re-image the installed dev channel as squashfs | `rk device package squashfs` |
42
+ | Get a version onto a channel, staged or live | Upload the signed `.pkg` in the Roku Developer Dashboard; no `rk` command does this |
43
+ | DCLs on a device | `rk dcl ls`, `rk dcl delete <filename>`, `rk dcl delete-all` |
44
+ | A partner's library the app loads at runtime | Build it as its own app with `app.type: "dcl"` in `roku-config.ts`; the host resolves it from the channel store by library name |
45
+ | Referencing a node out of that library | `getComponentLibraryFactory()` from `@roku-sdk/runtime/component-library`, then `factory.createLibrary(<name>)`, `library.load()` and `getDCLComponent(library, <node>)` from `@roku-sdk/rsg-ts` — full walkthrough in `documentation/core-concepts/dcl.md` |
46
+
47
+ ## Instructions
48
+
49
+ 1. **The signing password resolves from three places, in one order.** `--signing-password`
50
+ first, then the `ROKU_SIGNING_PASSWORD` environment variable, then a masked prompt. The
51
+ prompt needs a TTY on both stdin and stdout; without one the command fails immediately and
52
+ names the first two. A CI job therefore has exactly one tier it can use — the environment
53
+ variable. The flag is the wrong answer there, because the value lands in the process list and
54
+ in any log that echoes the command it was given.
55
+
56
+ 2. **`rk device package sign` signs the app on the device, not a file on disk.** It requires a box with
57
+ a dev channel already installed and keyed; a box without one fails saying so, and the fix is
58
+ to sideload first. The signed file lands in `--out`, falling back to `package.outDir` from
59
+ `roku-config.ts`, and then to the working directory. Name `--out` in a job, so the step that
60
+ collects the artifact does not depend on where the command ran.
61
+
62
+ 3. **`rekey` finds its package from the project when it is not given one.** The positional wins;
63
+ otherwise `package.signingKeyPackage` is used, and with neither the command explains both.
64
+ `--dev-id` falls back to `package.devId` the same way, and it is an assertion rather than an
65
+ input: the developer ID the box reports afterwards is checked against it.
66
+
67
+ 4. **Putting a version on a channel happens in the Developer Dashboard.** The SDK signs; it does
68
+ not upload, stage, wait on review or promote. Whether a version goes live or stays with QA is
69
+ chosen there, against the signed `.pkg` this skill produced. Do not offer an `rk` command or
70
+ flag for any of those steps, and do not read a `publishing` block in `roku-config.ts` as
71
+ evidence of one: the block is accepted, and `rk` does not read it.
72
+
73
+ 5. **Only a signed `.pkg` goes onto a channel.** What `rk build` writes to `dist-build` is
74
+ unsigned: side-load it, then sign it on the device with `rk device package sign`. That signed `.pkg`
75
+ is the file to upload.
76
+
77
+ 6. **`device package` and `dcl` name a box.** Both take `--device` for a box in the store, or `--host`
78
+ with `--password` for one by address, and both work the same whether the command went through
79
+ a running server or `--local`. The two ways of naming a box are exclusive: `--host` together
80
+ with `--device` is an error, and `--host` alone still needs a password, because an address is
81
+ not in the store to carry one.
82
+
83
+ ## Example
84
+
85
+ A CI job that signs without a prompt, keeping the password out of the command line and putting
86
+ the artifact in a fixed directory for the job to collect.
87
+
88
+ ```bash
89
+ # ROKU_SIGNING_PASSWORD comes from the job's secret store, never from an argument.
90
+ # --out fixes where the .pkg lands, whatever directory the job runs in.
91
+ rk device package sign --device ci-box --out dist-build/package --json
92
+ ```
93
+
94
+ The `.pkg` in `dist-build/package` is then uploaded in the Roku Developer Dashboard, where the
95
+ version is kept for review or published.
96
+
97
+ ## Common Mistakes
98
+
99
+ | Symptom | Cause | Fix |
100
+ |---|---|---|
101
+ | Signing fails in CI asking for a password, and works on a laptop | The prompt is the only tier left, and it needs a TTY | Set `ROKU_SIGNING_PASSWORD` in the job environment |
102
+ | The signed file is not where the next step looks | `--out` was omitted, so it fell back to `package.outDir` or the working directory | Pass `--out`, or set `package.outDir` to the directory that step reads |
103
+ | An unsigned build is offered for upload | The output of `rk build` was taken as the release artifact | Sideload it, then `rk device package sign`; only the signed `.pkg` uploads |
104
+ | An agent proposes an `rk` command to upload, stage or promote a version | The SDK has none | Sign with `rk device package sign`, then upload in the Roku Developer Dashboard |
105
+ | `rekey` reports the wrong developer ID | `--dev-id` or `package.devId` asserts the expected one and it did not match | Confirm which key the box should hold before rekeying it |
106
+ | Signing fails saying no dev channel is installed | Nothing is on the box to sign | Sideload the app first, then sign |
107
+ | `--host` is rejected, or reaches a box you did not mean | It cannot be combined with `--device`, and on its own it still needs `--password` | Give `--host` with `--password`, and drop `--device` — the store is not consulted for an address |
@@ -0,0 +1,133 @@
1
+ # App Performance
2
+
3
+ ## Overview
4
+
5
+ An RSG SDK TypeScript app that stutters, drops frames or grows its memory is diagnosed by
6
+ measuring it on the device, and there are two measurements: a Perfetto trace, which says where
7
+ the time went, and a CDP heap read, which says how much memory is held. They do not coexist —
8
+ the heap read rebuilds the app in debug mode — so decide which question you are asking before
9
+ recording anything. This skill covers taking the measurement and classifying what it shows.
10
+
11
+ ## When to Use
12
+
13
+ - an animation, scroll or transition that janks or drops frames on real hardware
14
+ - a key press whose effect arrives late, or a screen that takes too long to appear
15
+ - memory that climbs the longer the app is left running
16
+ - a recorded trace that has to be read: which thread, which slice, which interval blocks
17
+ - confirming that a change actually improved something, rather than assuming it did
18
+
19
+ ### When not to use
20
+
21
+ | Situation | Use instead | Because |
22
+ |---|---|---|
23
+ | The everyday deploy, screenshot, drive and log loop | The `mcp-ai-dev-loop` skill | Those tools answer what the app did, not what it cost |
24
+ | The tracing tools refuse, or no tools are present | The `mcp-setup` skill | The Perfetto tools need a running server; that is setup |
25
+ | A grid's own configuration or API | The `flex-grid` skill | Once a trace names the grid, the fix is that component's subject |
26
+ | A slow build, install or CI job | The `cli-dev-loop` skill | Nothing here measures anything but the running app; how the build and install steps are invoked belongs there |
27
+ | A test that never touches hardware | The `testing` skill | Timings from an in-process tree describe the host machine |
28
+ | A test that passes some runs and fails others, on a device or off it | The `testing` skill for an in-process test; for one that drives real hardware, no skill in this library covers it — take it to the repository's own device-test guidance | Flakiness is a determinism problem; nothing here measures whether a result is repeatable, and a trace of a passing run says nothing about the failing one |
29
+
30
+ ## Quick Reference
31
+
32
+ | Question | Call | Gives |
33
+ |---|---|---|
34
+ | Where did the frame time go | `perfetto_enable`, then `perfetto_start` / `perfetto_stop` | A trace file to open in the Perfetto UI |
35
+ | The same recording with no agent session | `rk perfetto` | The CLI spelling of those three calls; the analysis afterwards is identical and stays here |
36
+ | How much JS memory is held | `cdp_session_create`, then `cdp_heap_usage` | Total and used heap bytes |
37
+ | Which JavaScript function is hot, or what retains memory | The `cpu-and-heap-profiling` skill | A CPU profile or heap snapshot file |
38
+ | Release the debug session | `cdp_session_end` | One session per device; this frees it |
39
+ | Which TypeScript ran inside a slow window | Redeploy with `deploy_app`'s `perfettoPaths` | Named slices for instrumented functions |
40
+ | Which callback fired | The `EventCallback` slice under `observer.callback`, in an instrumented build | Function name and source position |
41
+
42
+ ## Instructions
43
+
44
+ 1. **Pick the measurement from the question, not the other way round.** "It janks" is a trace.
45
+ "It grows" is a heap read. Recording the wrong one is expensive in a way that is not obvious
46
+ later, because both produce plausible-looking numbers.
47
+
48
+ 2. **Enable tracing before the app launches.** `perfetto_enable` configures the device; the app
49
+ reads that configuration at startup. Enabling after the app is already running yields a trace
50
+ missing exactly the early work most worth seeing, and it looks like a successful recording.
51
+ The order is `perfetto_enable`, then deploy, then `perfetto_start` — reproduce the problem —
52
+ then `perfetto_stop`.
53
+
54
+ 3. **Read the trace against the render threads first.** `RenderThr:Main` carries `render` and
55
+ `swapBuffers` and is where the frame budget is decided; `AuxRenderThread` carries work that
56
+ can steal from it, and `consumeAllTasks` running on either during an animation is one of the
57
+ three usual causes of jank. TypeScript work arrives under the `rokuos.typescript` category.
58
+ Locate those in the track list before reading a single slice, because every rule below is
59
+ stated relative to where a slice sits.
60
+
61
+ 4. **The frame budget is `render` plus `swapBuffers` inside 16.67 ms.** Past that the frame is
62
+ dropped. The usual causes are one long `render`, meaning a heavy scene or too many nodes, or
63
+ an `observer.callback` over 1 ms delaying the first `render`.
64
+
65
+ 5. **Only two transaction intervals block, and only in one direction.** Going from TypeScript to
66
+ the render thread, `transaction.start` to `transaction.commit` blocks, and so does
67
+ `transaction.update` to `transaction.end`; over roughly 300 ms during an animation is what
68
+ makes a smooth scroll jank. The trace points carrying `toTS` are the other direction, and a
69
+ wide span there does **not** stall the render thread. Reading one as a stall is the most
70
+ common way to spend a day on the wrong slice.
71
+
72
+ 6. **Attribute the inside of a slow window by instrumenting, not by guessing.** Redeploying with
73
+ `deploy_app` and `perfettoPaths: ["packages/solid/src"]` — `--perfetto-paths` is the same
74
+ thing spelled for a terminal — names the reactive work inside the interval, where a
75
+ `createElement` slice means a SceneGraph node was created — during an animation that is
76
+ itself the finding. Widen to `solid-js` only if the narrower path is not enough: each
77
+ tracepoint costs roughly 10–30 microseconds per call, so a broad path distorts what you came
78
+ to measure. The value is matched as a substring of the source file path, so it depends on where
79
+ the SDK sits: `packages/solid/src` inside this workspace, `@roku-sdk/solid/src` in an app that
80
+ installed it. A path that matches nothing instruments nothing and reports no error, which reads
81
+ as an interval with no TypeScript in it. Naming every `createSignal` and `createMemo` makes
82
+ reactive slices attributable, but only in a build with `solidDevMode: true` at the top level
83
+ of `roku-config.ts` built with `rk dev --debug` (or `--solid-dev-mode` on any JS build), which is
84
+ off by default. Plain `rk dev` ignores the config field, and a build that precompiles to Hermes
85
+ bytecode skips it, so reactive work in a production trace stays anonymous no matter how
86
+ the config is set. The switch also adds overhead at component and signal creation, which is
87
+ one more reason the trace it names is not the one to quote timings from.
88
+
89
+ 7. **A heap read changes the build, so never compare its timings with a trace.**
90
+ `cdp_session_create` builds and sideloads unminified JavaScript with the debug preset
91
+ (debugger probe stubs, config overrides); plain `rk dev` also ships JavaScript by default. `cdp_heap_usage` reports total and used bytes from that app. Memory numbers
92
+ are meaningful, timings from that build are not. Only one session may exist per device, so
93
+ end it with `cdp_session_end` before starting another.
94
+
95
+ 8. **One trace is not evidence.** A TypeScript slice that spikes may have had garbage collection
96
+ run inside it, and one that spikes on a render thread may simply have been scheduled away.
97
+ Trust what repeats across frames, and re-record the same interaction after a fix rather than
98
+ declaring the fix worked.
99
+
100
+ 9. **Remove what you instrumented.** Extra tracepoint paths, temporary slices and debug builds
101
+ all outlive the investigation if nothing takes them out. Redeploy normally before handing the
102
+ app back, or the next person measures your instrumentation.
103
+
104
+ ## Example
105
+
106
+ Recording a scroll that janks, in the order the calls have to happen:
107
+
108
+ ```text
109
+ perfetto_enable {} -> device configured; must precede launch
110
+ deploy_app { root } -> the app starts with tracing on
111
+ perfetto_start {} -> returns sessionId and filePath
112
+ -- reproduce the jank now
113
+ perfetto_stop { sessionId } -> filePath, bytes, durationMs
114
+ ```
115
+
116
+ Then, in the trace: find the `keyEvent` that starts the scroll, follow the run of `render` and
117
+ `swapBuffers` after it on `RenderThr:Main`, and measure the first frame that exceeds 16.67 ms.
118
+ Everything overlapping that timestamp on the other two threads is the candidate set; a
119
+ `transaction.start` to `transaction.commit` gap spanning it is the answer, and a `toTS` span
120
+ spanning it is not.
121
+
122
+ ## Common Mistakes
123
+
124
+ | Symptom | Cause | Fix |
125
+ |---|---|---|
126
+ | The trace is missing the startup or first-animation work | Tracing was enabled after the app launched | `perfetto_enable` first, then deploy, then `perfetto_start` |
127
+ | A wide transaction span sits over the dropped frame but changing that code does nothing | It was a `toTS` span, which does not block the render thread | Measure `transaction.start` to `transaction.commit` instead |
128
+ | Timings look far worse than users report | They were read from a build made for a heap session, which has no Hermes bytecode | Trace a normal build; use the debug build only for memory |
129
+ | `cdp_session_create` is refused | A session already exists for that device | `cdp_session_end`, then create the new one |
130
+ | Instrumented slices are everywhere and nothing is legible | The instrumentation path was widened before the narrow one was read | Start at `packages/solid/src` and add more only if it falls short |
131
+ | Reactive work shows up unnamed | Named slices need solid's development build, which a production build skips outright | Set `solidDevMode: true` and redeploy with `rk dev --debug`, or pass `--solid-dev-mode` |
132
+ | A fix looks like an improvement in one recording | A single spike can be garbage collection or a context switch | Re-record and compare what repeats across frames |
133
+ | The tracing tools report that they need a server | Perfetto tools are server-mode only | Start one — see the `mcp-setup` skill |