@decocms/start 0.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.
- package/.cursor/skills/deco-api-call-dedup/SKILL.md +443 -0
- package/.cursor/skills/deco-apps-architecture/SKILL.md +255 -0
- package/.cursor/skills/deco-apps-architecture/app-pattern.md +288 -0
- package/.cursor/skills/deco-apps-architecture/commerce-types.md +239 -0
- package/.cursor/skills/deco-apps-architecture/new-app-guide.md +268 -0
- package/.cursor/skills/deco-apps-architecture/scripts-codegen.md +148 -0
- package/.cursor/skills/deco-apps-architecture/shared-utils.md +181 -0
- package/.cursor/skills/deco-apps-architecture/vtex-deep-structure.md +253 -0
- package/.cursor/skills/deco-apps-architecture/website-app.md +169 -0
- package/.cursor/skills/deco-apps-vtex-porting/SKILL.md +189 -0
- package/.cursor/skills/deco-apps-vtex-porting/adaptation-patterns.md +335 -0
- package/.cursor/skills/deco-apps-vtex-porting/commerce-porting.md +155 -0
- package/.cursor/skills/deco-apps-vtex-porting/cookie-auth-patterns.md +148 -0
- package/.cursor/skills/deco-apps-vtex-porting/structure-map.md +234 -0
- package/.cursor/skills/deco-apps-vtex-porting/transform-mapping.md +99 -0
- package/.cursor/skills/deco-apps-vtex-porting/website-porting.md +194 -0
- package/.cursor/skills/deco-apps-vtex-review/SKILL.md +234 -0
- package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +270 -0
- package/.cursor/skills/deco-async-rendering-site-guide/SKILL.md +417 -0
- package/.cursor/skills/deco-cms-layout-caching/SKILL.md +293 -0
- package/.cursor/skills/deco-cms-route-config/SKILL.md +388 -0
- package/.cursor/skills/deco-core-architecture/SKILL.md +185 -0
- package/.cursor/skills/deco-core-architecture/blocks.md +196 -0
- package/.cursor/skills/deco-core-architecture/deco-vs-deco-start.md +191 -0
- package/.cursor/skills/deco-core-architecture/engine.md +220 -0
- package/.cursor/skills/deco-core-architecture/hooks-components.md +157 -0
- package/.cursor/skills/deco-core-architecture/plugins-clients.md +136 -0
- package/.cursor/skills/deco-core-architecture/runtime.md +116 -0
- package/.cursor/skills/deco-core-architecture/site-usage.md +165 -0
- package/.cursor/skills/deco-e2e-testing/SKILL.md +372 -0
- package/.cursor/skills/deco-e2e-testing/discovery.md +337 -0
- package/.cursor/skills/deco-e2e-testing/scripts/scaffold.sh +81 -0
- package/.cursor/skills/deco-e2e-testing/selectors.md +175 -0
- package/.cursor/skills/deco-e2e-testing/templates/package.json +18 -0
- package/.cursor/skills/deco-e2e-testing/templates/playwright.config.ts +65 -0
- package/.cursor/skills/deco-e2e-testing/templates/scripts/baseline.ts +279 -0
- package/.cursor/skills/deco-e2e-testing/templates/scripts/run-e2e.ts +194 -0
- package/.cursor/skills/deco-e2e-testing/templates/specs/ecommerce-flow.spec.ts +612 -0
- package/.cursor/skills/deco-e2e-testing/templates/tsconfig.json +12 -0
- package/.cursor/skills/deco-e2e-testing/templates/utils/metrics-collector.ts +918 -0
- package/.cursor/skills/deco-e2e-testing/troubleshooting.md +602 -0
- package/.cursor/skills/deco-edge-caching/SKILL.md +316 -0
- package/.cursor/skills/deco-full-analysis/SKILL.md +898 -0
- package/.cursor/skills/deco-full-analysis/checklists/asset-optimization.md +251 -0
- package/.cursor/skills/deco-full-analysis/checklists/bug-fix.md +189 -0
- package/.cursor/skills/deco-full-analysis/checklists/cache-strategy.md +144 -0
- package/.cursor/skills/deco-full-analysis/checklists/dependency-update.md +150 -0
- package/.cursor/skills/deco-full-analysis/checklists/hydration-fix.md +191 -0
- package/.cursor/skills/deco-full-analysis/checklists/image-optimization.md +180 -0
- package/.cursor/skills/deco-full-analysis/checklists/loader-optimization.md +165 -0
- package/.cursor/skills/deco-full-analysis/checklists/seo-fix.md +183 -0
- package/.cursor/skills/deco-full-analysis/checklists/site-cleanup.md +281 -0
- package/.cursor/skills/deco-full-analysis/discovery.md +548 -0
- package/.cursor/skills/deco-incident-debugging/SKILL.md +378 -0
- package/.cursor/skills/deco-incident-debugging/headless-mode.md +510 -0
- package/.cursor/skills/deco-incident-debugging/learnings-index.md +227 -0
- package/.cursor/skills/deco-incident-debugging/triage-workflow.md +312 -0
- package/.cursor/skills/deco-islands-migration/SKILL.md +251 -0
- package/.cursor/skills/deco-loader-n-plus-1-detector/SKILL.md +275 -0
- package/.cursor/skills/deco-performance-audit/SKILL.md +530 -0
- package/.cursor/skills/deco-performance-audit/tools-reference.md +428 -0
- package/.cursor/skills/deco-performance-audit/workflow.md +457 -0
- package/.cursor/skills/deco-server-functions-invoke/SKILL.md +92 -0
- package/.cursor/skills/deco-server-functions-invoke/architecture.md +166 -0
- package/.cursor/skills/deco-server-functions-invoke/generator.md +122 -0
- package/.cursor/skills/deco-server-functions-invoke/problem.md +98 -0
- package/.cursor/skills/deco-server-functions-invoke/troubleshooting.md +110 -0
- package/.cursor/skills/deco-site-deployment/SKILL.md +396 -0
- package/.cursor/skills/deco-site-memory-debugging/SKILL.md +121 -0
- package/.cursor/skills/deco-site-memory-debugging/cdp-connection.md +222 -0
- package/.cursor/skills/deco-site-memory-debugging/memory-analysis.md +362 -0
- package/.cursor/skills/deco-site-patterns/SKILL.md +124 -0
- package/.cursor/skills/deco-site-patterns/app-composition.md +337 -0
- package/.cursor/skills/deco-site-patterns/client-patterns.md +341 -0
- package/.cursor/skills/deco-site-patterns/cms-wiring.md +230 -0
- package/.cursor/skills/deco-site-patterns/section-patterns.md +340 -0
- package/.cursor/skills/deco-site-scaling-tuning/SKILL.md +240 -0
- package/.cursor/skills/deco-site-scaling-tuning/analysis-scripts.md +267 -0
- package/.cursor/skills/deco-start-architecture/SKILL.md +218 -0
- package/.cursor/skills/deco-start-architecture/admin-protocol.md +156 -0
- package/.cursor/skills/deco-start-architecture/cms-resolution.md +201 -0
- package/.cursor/skills/deco-start-architecture/code-quality.md +158 -0
- package/.cursor/skills/deco-start-architecture/gap-analysis.md +129 -0
- package/.cursor/skills/deco-start-architecture/sdk-utilities.md +197 -0
- package/.cursor/skills/deco-start-architecture/worker-entry-caching.md +154 -0
- package/.cursor/skills/deco-startup-analysis/SKILL.md +248 -0
- package/.cursor/skills/deco-storefront-test-checklist/SKILL.md +369 -0
- package/.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md +468 -0
- package/.cursor/skills/deco-tanstack-navigation/SKILL.md +681 -0
- package/.cursor/skills/deco-tanstack-search/SKILL.md +411 -0
- package/.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md +1013 -0
- package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +518 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +719 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +96 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
- package/.cursor/skills/deco-typescript-fixes/SKILL.md +178 -0
- package/.cursor/skills/deco-typescript-fixes/common-fixes.md +330 -0
- package/.cursor/skills/deco-typescript-fixes/strategy.md +148 -0
- package/.cursor/skills/deco-variant-selection-perf/SKILL.md +272 -0
- package/.cursor/skills/deco-vtex-fetch-cache/SKILL.md +225 -0
- package/.cursor/skills/find-skills/SKILL.md +133 -0
- package/.cursor/skills/incident-report/SKILL.md +179 -0
- package/.cursor/skills/incident-report/references/5-whys.md +75 -0
- package/.cursor/skills/incident-report/templates/client-report.md +187 -0
- package/.cursor/skills/incident-report/templates/internal-report.md +206 -0
- package/.cursor/skills/template-skill/SKILL.md +38 -0
- package/.github/workflows/release.yml +32 -0
- package/.releaserc.json +25 -0
- package/CLAUDE.md +135 -0
- package/GAP_ANALYSIS.md +224 -0
- package/GAP_ANALYSIS_V2.md +1013 -0
- package/biome.json +39 -0
- package/knip.json +5 -0
- package/package.json +87 -0
- package/scripts/generate-blocks.ts +69 -0
- package/scripts/generate-invoke.ts +378 -0
- package/scripts/generate-schema.ts +657 -0
- package/src/admin/cors.ts +29 -0
- package/src/admin/decofile.ts +72 -0
- package/src/admin/index.ts +24 -0
- package/src/admin/invoke.ts +163 -0
- package/src/admin/liveControls.ts +29 -0
- package/src/admin/meta.ts +70 -0
- package/src/admin/render.ts +205 -0
- package/src/admin/schema.ts +686 -0
- package/src/admin/setup.ts +44 -0
- package/src/cms/index.ts +59 -0
- package/src/cms/loader.ts +180 -0
- package/src/cms/registry.ts +162 -0
- package/src/cms/resolve.ts +1005 -0
- package/src/cms/sectionLoaders.ts +294 -0
- package/src/hooks/DecoPageRenderer.tsx +444 -0
- package/src/hooks/LazySection.tsx +109 -0
- package/src/hooks/LiveControls.tsx +108 -0
- package/src/hooks/SectionErrorFallback.tsx +85 -0
- package/src/hooks/index.ts +8 -0
- package/src/index.ts +5 -0
- package/src/matchers/builtins.ts +184 -0
- package/src/matchers/posthog.ts +154 -0
- package/src/middleware/decoState.ts +55 -0
- package/src/middleware/healthMetrics.ts +131 -0
- package/src/middleware/index.ts +80 -0
- package/src/middleware/liveness.ts +21 -0
- package/src/middleware/observability.ts +205 -0
- package/src/routes/adminRoutes.ts +83 -0
- package/src/routes/cmsRoute.ts +302 -0
- package/src/routes/components.tsx +34 -0
- package/src/routes/index.ts +15 -0
- package/src/sdk/analytics.ts +72 -0
- package/src/sdk/cacheHeaders.ts +268 -0
- package/src/sdk/cachedLoader.ts +206 -0
- package/src/sdk/clx.ts +3 -0
- package/src/sdk/cookie.ts +39 -0
- package/src/sdk/createInvoke.ts +57 -0
- package/src/sdk/csp.ts +59 -0
- package/src/sdk/env.ts +27 -0
- package/src/sdk/index.ts +63 -0
- package/src/sdk/instrumentedFetch.ts +137 -0
- package/src/sdk/invoke.ts +133 -0
- package/src/sdk/mergeCacheControl.ts +150 -0
- package/src/sdk/redirects.ts +217 -0
- package/src/sdk/requestContext.ts +184 -0
- package/src/sdk/serverTimings.ts +68 -0
- package/src/sdk/signal.ts +41 -0
- package/src/sdk/sitemap.ts +143 -0
- package/src/sdk/urlUtils.ts +117 -0
- package/src/sdk/useDevice.ts +82 -0
- package/src/sdk/useId.ts +7 -0
- package/src/sdk/useScript.ts +101 -0
- package/src/sdk/workerEntry.ts +703 -0
- package/src/sdk/wrapCaughtErrors.ts +107 -0
- package/src/types/index.ts +39 -0
- package/src/types/widgets.ts +13 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# CDP Connection Guide
|
|
2
|
+
|
|
3
|
+
How to connect to a Deno pod's Chrome DevTools Protocol inspector for memory debugging.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- `kubectl` configured with cluster access
|
|
8
|
+
- Python 3 with `websockets` package (`pip3 install websockets`)
|
|
9
|
+
- Pod must be running Deno with inspector enabled (Deco sites expose port 9229 by default)
|
|
10
|
+
|
|
11
|
+
## Step 1: Identify the Pod
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# List pods in the site namespace
|
|
15
|
+
kubectl get pods -n sites-<sitename> -o wide
|
|
16
|
+
|
|
17
|
+
# Check current memory usage
|
|
18
|
+
kubectl top pods -n sites-<sitename>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Step 2: Port-Forward
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Forward local 9229 to pod's inspector port
|
|
25
|
+
kubectl port-forward -n sites-<sitename> <pod-name> 9229:9229
|
|
26
|
+
|
|
27
|
+
# If port 9229 is busy, use a different local port
|
|
28
|
+
kubectl port-forward -n sites-<sitename> <pod-name> 19229:9229
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Keep this running in a separate terminal.** Port-forwards drop frequently — check if it's alive before running scripts.
|
|
32
|
+
|
|
33
|
+
## Step 3: Get WebSocket URL
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
curl -s http://127.0.0.1:9229/json/list | jq '.[0].webSocketDebuggerUrl'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This returns something like:
|
|
40
|
+
```
|
|
41
|
+
ws://127.0.0.1:9229/ws/b9cf0f05-6e67-4ad6-865f-f418f6b4856c
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**The UUID changes every time the pod restarts.** Always fetch a fresh URL.
|
|
45
|
+
|
|
46
|
+
## Step 4: Connect via Python
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import asyncio, json, websockets
|
|
50
|
+
|
|
51
|
+
WS = "ws://127.0.0.1:9229/ws/<UUID>"
|
|
52
|
+
MSG_ID = 0
|
|
53
|
+
|
|
54
|
+
async def send_cmd(ws, method, params=None):
|
|
55
|
+
global MSG_ID
|
|
56
|
+
MSG_ID += 1
|
|
57
|
+
msg = {"id": MSG_ID, "method": method}
|
|
58
|
+
if params:
|
|
59
|
+
msg["params"] = params
|
|
60
|
+
await ws.send(json.dumps(msg))
|
|
61
|
+
for _ in range(10000):
|
|
62
|
+
raw = await asyncio.wait_for(ws.recv(), timeout=30)
|
|
63
|
+
data = json.loads(raw)
|
|
64
|
+
if data.get("id") == MSG_ID:
|
|
65
|
+
return data
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
async def evaluate(ws, expr):
|
|
69
|
+
r = await send_cmd(ws, "Runtime.evaluate", {
|
|
70
|
+
"expression": expr,
|
|
71
|
+
"contextId": 1, # IMPORTANT: always use contextId: 1
|
|
72
|
+
"returnByValue": True,
|
|
73
|
+
"awaitPromise": True,
|
|
74
|
+
"timeout": 30000,
|
|
75
|
+
})
|
|
76
|
+
if r and "result" in r and "result" in r["result"]:
|
|
77
|
+
return r["result"]["result"].get("value")
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
async def main():
|
|
81
|
+
async with websockets.connect(WS, max_size=50*1024*1024) as ws:
|
|
82
|
+
await send_cmd(ws, "Runtime.enable")
|
|
83
|
+
# Your analysis code here...
|
|
84
|
+
|
|
85
|
+
asyncio.run(main())
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Common Mistakes and Pitfalls
|
|
89
|
+
|
|
90
|
+
### 1. Missing `contextId: 1`
|
|
91
|
+
|
|
92
|
+
**Symptom:** `Runtime.evaluate` returns empty results or "Cannot find default execution context".
|
|
93
|
+
|
|
94
|
+
**Cause:** After `Runtime.enable`, Deno emits thousands of `Runtime.consoleAPICalled` events that flood the event loop. Without explicit `contextId`, the evaluation may target the wrong context.
|
|
95
|
+
|
|
96
|
+
**Fix:** Always pass `contextId: 1` in every `Runtime.evaluate` call:
|
|
97
|
+
```python
|
|
98
|
+
await send_cmd(ws, "Runtime.evaluate", {
|
|
99
|
+
"expression": expr,
|
|
100
|
+
"contextId": 1, # <-- THIS IS REQUIRED
|
|
101
|
+
...
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 2. queryObjects returns under `objects`, not `result`
|
|
106
|
+
|
|
107
|
+
**Symptom:** `KeyError: 'result'` when accessing `queryObjects` response.
|
|
108
|
+
|
|
109
|
+
**Cause:** Deno's V8 inspector returns the array under `result.objects`, not `result.result` like Chrome does.
|
|
110
|
+
|
|
111
|
+
**Fix:**
|
|
112
|
+
```python
|
|
113
|
+
# WRONG (Chrome-style):
|
|
114
|
+
arr_id = qr["result"]["result"].get("objectId")
|
|
115
|
+
|
|
116
|
+
# CORRECT (Deno-style):
|
|
117
|
+
arr_id = qr["result"].get("objects", {}).get("objectId")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Full pattern:
|
|
121
|
+
```python
|
|
122
|
+
async def query_objects(ws, proto_expr):
|
|
123
|
+
proto_r = await send_cmd(ws, "Runtime.evaluate", {
|
|
124
|
+
"expression": proto_expr,
|
|
125
|
+
"contextId": 1,
|
|
126
|
+
})
|
|
127
|
+
proto_id = proto_r["result"].get("result", {}).get("objectId")
|
|
128
|
+
if not proto_id:
|
|
129
|
+
return None
|
|
130
|
+
qr = await send_cmd(ws, "Runtime.queryObjects", {"prototypeObjectId": proto_id})
|
|
131
|
+
if not qr or "result" not in qr:
|
|
132
|
+
return None
|
|
133
|
+
return qr["result"].get("objects", {}).get("objectId")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 3. Event flooding drowns responses
|
|
137
|
+
|
|
138
|
+
**Symptom:** `send_cmd` never receives the response, times out.
|
|
139
|
+
|
|
140
|
+
**Cause:** `Runtime.enable` triggers a flood of `Runtime.consoleAPICalled` events (can be thousands). The event drain loop in `send_cmd` may exhaust its iteration limit before finding the response.
|
|
141
|
+
|
|
142
|
+
**Fix:** Increase the drain limit:
|
|
143
|
+
```python
|
|
144
|
+
for _ in range(10000): # Use 10000, not 100
|
|
145
|
+
raw = await asyncio.wait_for(ws.recv(), timeout=30)
|
|
146
|
+
data = json.loads(raw)
|
|
147
|
+
if data.get("id") == MSG_ID:
|
|
148
|
+
return data
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 4. Port-forward drops silently
|
|
152
|
+
|
|
153
|
+
**Symptom:** `ConnectionClosedError: no close frame` or `ConnectionRefusedError`.
|
|
154
|
+
|
|
155
|
+
**Cause:** `kubectl port-forward` drops connections under load or after inactivity.
|
|
156
|
+
|
|
157
|
+
**Fix:**
|
|
158
|
+
1. Check if port-forward process is still running
|
|
159
|
+
2. Re-establish port-forward
|
|
160
|
+
3. Get a fresh WebSocket URL (same pod, new connection)
|
|
161
|
+
4. Script should handle reconnection gracefully
|
|
162
|
+
|
|
163
|
+
### 5. WebSocket message too large
|
|
164
|
+
|
|
165
|
+
**Symptom:** `PayloadTooBig` error when taking heap snapshots.
|
|
166
|
+
|
|
167
|
+
**Fix:** Increase `max_size` on the WebSocket connection:
|
|
168
|
+
```python
|
|
169
|
+
async with websockets.connect(WS, max_size=100*1024*1024) as ws:
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 6. Deno 2.x API changes
|
|
173
|
+
|
|
174
|
+
Some APIs changed in Deno 2.x:
|
|
175
|
+
- `Deno.resources()` → **removed** in Deno 2.x. Use `/proc/self/fd` instead.
|
|
176
|
+
- `caches.keys()` → may not be available as a function. Use `caches.open(name)` and `cache.keys()` instead.
|
|
177
|
+
|
|
178
|
+
### 7. /proc access requires permissions
|
|
179
|
+
|
|
180
|
+
**Symptom:** "Requires all access" error when reading `/proc/self/status` or `/proc/self/maps`.
|
|
181
|
+
|
|
182
|
+
**Cause:** Deno's permission system blocks filesystem reads outside allowed paths.
|
|
183
|
+
|
|
184
|
+
**Workaround:** Use `Deno.memoryUsage()` instead — it doesn't require extra permissions and gives RSS, heap, and external memory.
|
|
185
|
+
|
|
186
|
+
### 8. Heap snapshot node_fields empty
|
|
187
|
+
|
|
188
|
+
**Symptom:** Heap snapshot `snapshot.node_fields` is an empty array.
|
|
189
|
+
|
|
190
|
+
**Cause:** Deno/V8 stores `node_fields` under `snapshot.meta` or uses a different layout than Chrome.
|
|
191
|
+
|
|
192
|
+
**Fix:** Infer field count from `len(nodes) / node_count`. Standard V8 uses 6 or 7 fields per node:
|
|
193
|
+
```python
|
|
194
|
+
field_count = len(nodes) // node_count
|
|
195
|
+
# 6 fields: type, name, id, self_size, edge_count, trace_node_id
|
|
196
|
+
# 7 fields: + detachedness
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 9. callFunctionOn for object analysis
|
|
200
|
+
|
|
201
|
+
After `queryObjects`, use `callFunctionOn` to analyze the returned array:
|
|
202
|
+
```python
|
|
203
|
+
async def call_on(ws, obj_id, func):
|
|
204
|
+
r = await send_cmd(ws, "Runtime.callFunctionOn", {
|
|
205
|
+
"objectId": obj_id,
|
|
206
|
+
"functionDeclaration": func,
|
|
207
|
+
"returnByValue": True,
|
|
208
|
+
})
|
|
209
|
+
if not r or "result" not in r:
|
|
210
|
+
return None
|
|
211
|
+
return r["result"].get("result", {}).get("value")
|
|
212
|
+
|
|
213
|
+
# Example: count Response objects and check bodyUsed
|
|
214
|
+
body_info = await call_on(ws, resp_array_id, """function() {
|
|
215
|
+
let used = 0, notUsed = 0;
|
|
216
|
+
for (let i = 0; i < this.length; i++) {
|
|
217
|
+
if (this[i].bodyUsed) used++;
|
|
218
|
+
else notUsed++;
|
|
219
|
+
}
|
|
220
|
+
return JSON.stringify({used, notUsed});
|
|
221
|
+
}""")
|
|
222
|
+
```
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# Memory Analysis Procedures
|
|
2
|
+
|
|
3
|
+
Step-by-step procedures for analyzing memory in Deno pods via CDP.
|
|
4
|
+
|
|
5
|
+
## Procedure 1: Quick Memory Check
|
|
6
|
+
|
|
7
|
+
**Goal:** Determine if memory is a leak or lazy GC. Takes 2 minutes.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
# 1. Get memory BEFORE GC
|
|
11
|
+
mem_before = await evaluate(ws, "JSON.stringify(Deno.memoryUsage())")
|
|
12
|
+
|
|
13
|
+
# 2. Force GC
|
|
14
|
+
await send_cmd(ws, "HeapProfiler.collectGarbage")
|
|
15
|
+
await asyncio.sleep(0.5)
|
|
16
|
+
await send_cmd(ws, "HeapProfiler.collectGarbage") # twice for thoroughness
|
|
17
|
+
|
|
18
|
+
# 3. Get memory AFTER GC
|
|
19
|
+
mem_after = await evaluate(ws, "JSON.stringify(Deno.memoryUsage())")
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Interpretation:**
|
|
23
|
+
- RSS drops >30%? → Lazy GC, not a leak. **Recommend reducing `--max-old-space-size`.**
|
|
24
|
+
- RSS drops <10%? → Real retained memory. Continue to Procedure 2.
|
|
25
|
+
|
|
26
|
+
**Recommendation for lazy GC:**
|
|
27
|
+
If most memory is reclaimable by GC, the pod doesn't have a leak — V8 is just being lazy about collecting garbage. Reduce the max old space size so GC triggers more frequently:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# In the deployment or Deno flags:
|
|
31
|
+
--v8-flags=--max-old-space-size=512
|
|
32
|
+
# or even 256 for small sites
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This keeps RSS predictable without affecting performance. V8's incremental GC is fast (typically <10ms pauses) so more frequent runs have negligible impact on request latency.
|
|
36
|
+
|
|
37
|
+
## Procedure 2: Object Leak Detection
|
|
38
|
+
|
|
39
|
+
**Goal:** Find leaked Response/Request objects and unconsumed bodies.
|
|
40
|
+
|
|
41
|
+
### Check Response Objects
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
resp_id = await query_objects(ws, "Response.prototype")
|
|
45
|
+
if resp_id:
|
|
46
|
+
info = await call_on(ws, resp_id, """function() {
|
|
47
|
+
let used = 0, notUsed = 0;
|
|
48
|
+
const leakedUrls = [];
|
|
49
|
+
for (let i = 0; i < this.length; i++) {
|
|
50
|
+
if (this[i].bodyUsed) used++;
|
|
51
|
+
else {
|
|
52
|
+
notUsed++;
|
|
53
|
+
if (leakedUrls.length < 20)
|
|
54
|
+
leakedUrls.push({
|
|
55
|
+
url: this[i].url.substring(0, 120),
|
|
56
|
+
status: this[i].status
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return JSON.stringify({total: this.length, used, notUsed, leakedUrls});
|
|
61
|
+
}""")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Interpretation:**
|
|
65
|
+
- `notUsed` < 5? → Normal (in-flight requests)
|
|
66
|
+
- `notUsed` > 50? → **Response body leak.** Bodies are fetched but never consumed (`.text()`, `.json()`, `.arrayBuffer()`, or `.body.cancel()`).
|
|
67
|
+
- Check the URLs to identify which code path is leaking
|
|
68
|
+
|
|
69
|
+
### Check Request Objects
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
req_id = await query_objects(ws, "Request.prototype")
|
|
73
|
+
if req_id:
|
|
74
|
+
info = await call_on(ws, req_id, """function() {
|
|
75
|
+
const hosts = {};
|
|
76
|
+
for (let i = 0; i < this.length; i++) {
|
|
77
|
+
try {
|
|
78
|
+
const h = new URL(this[i].url).host;
|
|
79
|
+
hosts[h] = (hosts[h] || 0) + 1;
|
|
80
|
+
} catch(e) {}
|
|
81
|
+
}
|
|
82
|
+
return JSON.stringify({total: this.length, hosts});
|
|
83
|
+
}""")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Interpretation:**
|
|
87
|
+
- Hundreds of Request objects to the same host → possible fetch loop or unbounded cache
|
|
88
|
+
- `localhost` requests → SSR self-fetches (normal for Fresh)
|
|
89
|
+
|
|
90
|
+
## Procedure 3: ArrayBuffer Analysis
|
|
91
|
+
|
|
92
|
+
**Goal:** Identify large memory consumers in ArrayBuffers.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
ab_id = await query_objects(ws, "ArrayBuffer.prototype")
|
|
96
|
+
if ab_id:
|
|
97
|
+
info = await call_on(ws, ab_id, """function() {
|
|
98
|
+
let totalBytes = 0;
|
|
99
|
+
const buckets = {
|
|
100
|
+
'0-1KB': 0, '1-10KB': 0, '10-100KB': 0,
|
|
101
|
+
'100KB-1MB': 0, '1-10MB': 0, '10MB+': 0
|
|
102
|
+
};
|
|
103
|
+
const large = [];
|
|
104
|
+
for (let i = 0; i < this.length; i++) {
|
|
105
|
+
const sz = this[i].byteLength;
|
|
106
|
+
totalBytes += sz;
|
|
107
|
+
if (sz < 1024) buckets['0-1KB']++;
|
|
108
|
+
else if (sz < 10240) buckets['1-10KB']++;
|
|
109
|
+
else if (sz < 102400) buckets['10-100KB']++;
|
|
110
|
+
else if (sz < 1048576) buckets['100KB-1MB']++;
|
|
111
|
+
else if (sz < 10485760) buckets['1-10MB']++;
|
|
112
|
+
else buckets['10MB+']++;
|
|
113
|
+
|
|
114
|
+
if (sz > 100000 && large.length < 20) {
|
|
115
|
+
try {
|
|
116
|
+
const preview = new TextDecoder().decode(
|
|
117
|
+
new Uint8Array(this[i], 0, Math.min(200, sz))
|
|
118
|
+
);
|
|
119
|
+
large.push({sizeMB: sz/1024/1024, preview});
|
|
120
|
+
} catch(e) {
|
|
121
|
+
large.push({sizeMB: sz/1024/1024, preview: '(binary)'});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return JSON.stringify({
|
|
126
|
+
count: this.length,
|
|
127
|
+
totalMB: totalBytes/1024/1024,
|
|
128
|
+
buckets,
|
|
129
|
+
large
|
|
130
|
+
});
|
|
131
|
+
}""")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Known ArrayBuffer patterns:**
|
|
135
|
+
- **~304 MB static buffer** — V8/ICU internal data. Always present. Ignore it.
|
|
136
|
+
- **`resourceMetrics` JSON buffers (0.3-0.6 MB each)** — OpenTelemetry export batches accumulating. Minor but grows over time.
|
|
137
|
+
- **Large JSON buffers (>1 MB)** — ProductListingPage or similar API responses. If appearing in PAIRS, might indicate response body read + original buffer both retained.
|
|
138
|
+
- **`data:application/json;base64,...`** — Source maps. Normal, proportional to loaded modules.
|
|
139
|
+
- **`<!DOCTYPE html>...`** — Rendered HTML pages. If many, SSR cache might be unbounded.
|
|
140
|
+
|
|
141
|
+
## Procedure 4: Heap Snapshot
|
|
142
|
+
|
|
143
|
+
**Goal:** Get a comprehensive view of all heap objects.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
await send_cmd(ws, "HeapProfiler.enable")
|
|
147
|
+
|
|
148
|
+
MSG_ID += 1
|
|
149
|
+
snap_id = MSG_ID
|
|
150
|
+
await ws.send(json.dumps({
|
|
151
|
+
"id": snap_id,
|
|
152
|
+
"method": "HeapProfiler.takeHeapSnapshot",
|
|
153
|
+
"params": {"reportProgress": False, "treatGlobalObjectsAsRoots": True}
|
|
154
|
+
}))
|
|
155
|
+
|
|
156
|
+
chunks = []
|
|
157
|
+
for _ in range(200000):
|
|
158
|
+
raw = await asyncio.wait_for(ws.recv(), timeout=120)
|
|
159
|
+
data = json.loads(raw)
|
|
160
|
+
if data.get("method") == "HeapProfiler.addHeapSnapshotChunk":
|
|
161
|
+
chunks.append(data["params"]["chunk"])
|
|
162
|
+
elif data.get("id") == snap_id:
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
snapshot = json.loads("".join(chunks))
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Parsing the snapshot:**
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
snap_meta = snapshot.get("snapshot", {})
|
|
172
|
+
node_count = snap_meta.get("node_count", 0)
|
|
173
|
+
nodes = snapshot.get("nodes", [])
|
|
174
|
+
strings = snapshot.get("strings", [])
|
|
175
|
+
|
|
176
|
+
# Infer field count (node_fields may be empty in Deno)
|
|
177
|
+
field_count = len(nodes) // node_count # typically 6 or 7
|
|
178
|
+
|
|
179
|
+
# V8 node type indices (standard order):
|
|
180
|
+
# 0=hidden, 1=array, 2=string, 3=object, 4=code,
|
|
181
|
+
# 5=closure, 6=regexp, 7=number, 8=native,
|
|
182
|
+
# 9=synthetic, 10=concatenated string, 11=sliced string,
|
|
183
|
+
# 12=symbol, 13=bigint, 14=object shape
|
|
184
|
+
|
|
185
|
+
# Aggregate by type
|
|
186
|
+
type_agg = {}
|
|
187
|
+
for i in range(0, node_count * field_count, field_count):
|
|
188
|
+
node_type = nodes[i] # index 0 = type
|
|
189
|
+
name_idx = nodes[i + 1] # index 1 = name (string table index)
|
|
190
|
+
self_size = nodes[i + 3] # index 3 = self_size
|
|
191
|
+
# aggregate...
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**What to look for in the snapshot:**
|
|
195
|
+
- `string` type >100 MB → HTML pages or JSON cached in memory
|
|
196
|
+
- `native` (type 8) → ArrayBuffers (cross-reference with Procedure 3)
|
|
197
|
+
- `closure` count very high → possible listener/callback leak
|
|
198
|
+
- `object` with specific names → identify which data structures hold memory
|
|
199
|
+
|
|
200
|
+
## Procedure 5: Additional Checks
|
|
201
|
+
|
|
202
|
+
### Open File Descriptors
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
fds = await evaluate(ws, """
|
|
206
|
+
(async () => {
|
|
207
|
+
try {
|
|
208
|
+
let count = 0;
|
|
209
|
+
const types = {socket: 0, pipe: 0, file: 0, other: 0};
|
|
210
|
+
for await (const entry of Deno.readDir('/proc/self/fd')) {
|
|
211
|
+
count++;
|
|
212
|
+
try {
|
|
213
|
+
const link = await Deno.readLink('/proc/self/fd/' + entry.name);
|
|
214
|
+
if (link.startsWith('socket:')) types.socket++;
|
|
215
|
+
else if (link.startsWith('pipe:')) types.pipe++;
|
|
216
|
+
else if (link.startsWith('/')) types.file++;
|
|
217
|
+
else types.other++;
|
|
218
|
+
} catch(e) { types.other++; }
|
|
219
|
+
}
|
|
220
|
+
return JSON.stringify({count, types});
|
|
221
|
+
} catch(e) { return JSON.stringify({error: e.message}); }
|
|
222
|
+
})()
|
|
223
|
+
""")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- 50-100 FDs → normal
|
|
227
|
+
- 500+ FDs → possible connection leak or file handle leak
|
|
228
|
+
|
|
229
|
+
### Map/Set Objects (potential caches)
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
map_id = await query_objects(ws, "Map.prototype")
|
|
233
|
+
if map_id:
|
|
234
|
+
info = await call_on(ws, map_id, """function() {
|
|
235
|
+
const large = [];
|
|
236
|
+
for (let i = 0; i < this.length; i++) {
|
|
237
|
+
if (this[i].size > 10) {
|
|
238
|
+
let keys = [];
|
|
239
|
+
let j = 0;
|
|
240
|
+
for (const k of this[i].keys()) {
|
|
241
|
+
if (j++ >= 3) break;
|
|
242
|
+
keys.push(String(k).substring(0, 80));
|
|
243
|
+
}
|
|
244
|
+
large.push({size: this[i].size, keys});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
large.sort((a,b) => b.size - a.size);
|
|
248
|
+
return JSON.stringify({total: this.length, large: large.slice(0, 15)});
|
|
249
|
+
}""")
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Deno Module Cache Size
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
cache_info = await evaluate(ws, """
|
|
256
|
+
(async () => {
|
|
257
|
+
const denoDir = Deno.env.get('DENO_DIR') || '/app/deco/deno_dir';
|
|
258
|
+
let count = 0, totalSize = 0;
|
|
259
|
+
async function walk(dir, depth) {
|
|
260
|
+
if (depth > 3) return;
|
|
261
|
+
try {
|
|
262
|
+
for await (const entry of Deno.readDir(dir)) {
|
|
263
|
+
if (entry.isDirectory) await walk(dir + '/' + entry.name, depth + 1);
|
|
264
|
+
else {
|
|
265
|
+
count++;
|
|
266
|
+
try {
|
|
267
|
+
const s = await Deno.stat(dir + '/' + entry.name);
|
|
268
|
+
totalSize += s.size;
|
|
269
|
+
} catch(e) {}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch(e) {}
|
|
273
|
+
}
|
|
274
|
+
await walk(denoDir, 0);
|
|
275
|
+
return JSON.stringify({denoDir, files: count, sizeMB: totalSize/1024/1024});
|
|
276
|
+
})()
|
|
277
|
+
""")
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Deno Version
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
ver = await evaluate(ws, """
|
|
284
|
+
JSON.stringify({
|
|
285
|
+
deno: Deno.version,
|
|
286
|
+
pid: Deno.pid,
|
|
287
|
+
hostname: Deno.hostname(),
|
|
288
|
+
})
|
|
289
|
+
""")
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Procedure 6: LRU Cache Inspection
|
|
293
|
+
|
|
294
|
+
**Important context:** Deco's LRU cache stores `true` (a boolean) as the value — it's a metadata index for the filesystem cache, NOT storing response bodies in memory. The `calculatedSize` is the sum of tracked Content-Length values (metadata tracking), not actual memory consumption.
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
lru = await evaluate(ws, """
|
|
298
|
+
(() => {
|
|
299
|
+
const results = [];
|
|
300
|
+
function findLRU(obj, path, depth) {
|
|
301
|
+
if (depth > 3 || !obj) return;
|
|
302
|
+
try {
|
|
303
|
+
for (const key of Object.keys(obj)) {
|
|
304
|
+
try {
|
|
305
|
+
const val = obj[key];
|
|
306
|
+
if (val && typeof val === 'object' &&
|
|
307
|
+
typeof val.max === 'number' &&
|
|
308
|
+
typeof val.size === 'number' &&
|
|
309
|
+
typeof val.calculatedSize === 'number') {
|
|
310
|
+
results.push({
|
|
311
|
+
path: path + '.' + key,
|
|
312
|
+
size: val.size,
|
|
313
|
+
calcSizeMB: val.calculatedSize / 1024 / 1024,
|
|
314
|
+
max: val.max,
|
|
315
|
+
maxSizeMB: val.maxSize ? val.maxSize / 1024 / 1024 : null,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (depth < 2) findLRU(val, path + '.' + key, depth + 1);
|
|
319
|
+
} catch(e) {}
|
|
320
|
+
}
|
|
321
|
+
} catch(e) {}
|
|
322
|
+
}
|
|
323
|
+
findLRU(globalThis, 'globalThis', 0);
|
|
324
|
+
return JSON.stringify(results);
|
|
325
|
+
})()
|
|
326
|
+
""")
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Interpretation:**
|
|
330
|
+
- `size` = number of entries in the LRU
|
|
331
|
+
- `calculatedSize` = sum of Content-Length values tracked (metadata, NOT actual memory)
|
|
332
|
+
- `max` = maximum number of entries
|
|
333
|
+
- `maxSize` = maximum calculatedSize before eviction
|
|
334
|
+
|
|
335
|
+
## Summary: What's Normal vs What's a Leak
|
|
336
|
+
|
|
337
|
+
| Metric | Normal Range | Concern Threshold |
|
|
338
|
+
|--------|-------------|-------------------|
|
|
339
|
+
| RSS after GC | 500-1500 MB | >2 GB or growing continuously |
|
|
340
|
+
| Heap used | 100-300 MB | >500 MB after GC |
|
|
341
|
+
| Response objects (bodyUsed=false) | <10 | >50 |
|
|
342
|
+
| ArrayBuffers (excl. static 304MB) | <100 MB | >500 MB |
|
|
343
|
+
| Open FDs | 50-100 | >500 |
|
|
344
|
+
| Promises | 1000-5000 | >50000 |
|
|
345
|
+
| RSS drop after GC | 10-50% | If <5%, memory is truly retained |
|
|
346
|
+
|
|
347
|
+
## Typical Healthy Memory Profile (after GC)
|
|
348
|
+
|
|
349
|
+
For a large Deco/Fresh site (e.g., fila-store, farmrio):
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
RSS: ~1500 MB
|
|
353
|
+
├── Heap: ~150 MB (JS objects)
|
|
354
|
+
├── External: ~350 MB (ArrayBuffers, ~304MB is static V8)
|
|
355
|
+
└── Native: ~1000 MB (Deno runtime + JIT + modules)
|
|
356
|
+
├── V8 JIT compiled code: ~300-500 MB
|
|
357
|
+
├── Deno Rust runtime: ~200-300 MB
|
|
358
|
+
├── Module cache (on disk): ~100-200 MB
|
|
359
|
+
└── Libraries + thread stacks: ~100 MB
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
The ~1 GB native gap is expected for apps loading thousands of modules. It's stable and does not grow over time.
|