Every interactive instrument's iframe stays in memory after it closes, retained by a theme observer on the parent document
メンテナーはふだん 3 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 83/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- typescript
- 領域
- frontend, performance, testing
調査の方向性
packages/runtime-internal/src/interactive/bootstrap.js:18-35と、指定されたpackages/runtime-internal/src/interactive/__tests__/bootstrap.test.tsのテストを読んでください。まずそのテストを実行し、偽の親ドキュメントをどのように設定してpagehideを発生させるかを確認します。pagehideの後にobserverが親のテーマをミラーしなくなれば完了です。issueではtesting/src/specs/instrument-completion.spec.tsでのエンドツーエンドの確認も提案されていますが、単体テストの動作を検証するために必須ではありません。
索引モデルが issue の本文から書いたものです。
説明
Every interactive instrument's iframe stays in memory after it is removed, until the host page is reloaded. bootstrap.js runs inside the iframe and registers a MutationObserver on the parent's <html> element to mirror the theme, and nothing ever calls disconnect(). Per the DOM spec a node holds a strong reference to the observers registered on it. So the parent's <html>, which lives for the whole SPA session, keeps the iframe's observer alive. The observer keeps its callback alive, and the callback is a closure in the iframe's realm (it reads the iframe's document), so the iframe's whole global stays reachable. That global holds every module the task loaded (jsPsych, React, the instrument, its data URL assets) and, through frameElement, the iframe element with the full bundle in data-bundle. In a clinic session that administers several interactive tasks, or a series with interactive items, each finished task's memory accumulates. On the tablets used for remote and in-person sessions this grows until the tab is reloaded or killed.
Where
packages/runtime-internal/src/interactive/bootstrap.js:18-35:
// Synchronize the iframe theme with the parent document
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === THEME_ATTRIBUTE) {
// ...
document.documentElement.setAttribute(THEME_ATTRIBUTE, updatedTheme);
// ...
});
observer.observe(parent.document.documentElement, {
attributes: true
});
Reproduce
I confirmed this in Chromium (Playwright's bundled build) with a parent page that mounts and removes five same-origin iframes. Each iframe runs the observer code above verbatim and stores a 2M-element array on its global, and the parent keeps a WeakRef to that array. After the iframes were removed I forced GC with HeapProfiler.collectGarbage:
- without the observer: 0 of 5 iframe globals alive, heap 0.6 MB
- with the observer as written: 5 of 5 iframe globals alive, heap 42.3 MB
- with the observer plus
addEventListener('pagehide', () => observer.disconnect()): 0 of 5 alive
In the app:
- In
apps/web, start a session and complete an interactive instrument (for example the Breakout Task). Then complete another one, or a series with two interactive items. - In DevTools > Memory, take a heap snapshot after each one and filter for
WindoworDetached.
Actual: each completed task leaves a detached iframe Window behind, retained through MutationObserver from the parent's HTMLHtmlElement, and heap usage grows with every task.
Expected: once its iframe is removed, nothing keeps an interactive instrument's realm alive.
Tests
packages/runtime-internal/src/interactive/__tests__/bootstrap.test.ts: it('should stop observing the parent theme on pagehide, so a removed iframe is not kept alive by the parent document'). Bootstrap against the existing fake parent document, dispatch pagehide on window, change the parent's data-mode, and assert that the iframe's data-mode no longer follows it.
e2e, in testing/src/specs/instrument-completion.spec.ts: after completing an interactive instrument, assert through CDP (HeapProfiler.collectGarbage, then Runtime.queryObjects on Window or a WeakRef probe) that the removed iframe's window has been collected.
Suggested fix
In bootstrap.js, add addEventListener('pagehide', () => observer.disconnect(), { once: true }). pagehide fires in the iframe when its element is removed. Also pass attributeFilter: [THEME_ATTRIBUTE] to observe, so the callback stops running on every other attribute change on the parent's <html>.
- 主要言語
- TypeScript
- スター
- 119
- フォーク
- 19
- 平均マージ
- 4日 12時間
- マージ済み PR(30日)
- 39
環境構築
- Dockerfile または Docker Compose ファイルあり
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
DouglasNeuroInformatics/OpenDataCapture のほかの issue
-
Area: Infrastructure Chore Difficulty: Low Priority: Medium
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
DouglasNeuroInformatics/OpenDataCapture#1835 ·
メンテナーはふだん 3 日以内に返信
-
Area: Instruments Bug Difficulty: Low Priority: Low
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
DouglasNeuroInformatics/OpenDataCapture#1824 · コメント 1 件 ·
メンテナーはふだん 3 日以内に返信
-
Completing a series instrument logs a TypeError "Cannot read properties of undefined (reading 'kind')"対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープンArea: Shared Bug Difficulty: Low Priority: Low
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
DouglasNeuroInformatics/OpenDataCapture#1813 · コメント 2 件 ·
メンテナーはふだん 3 日以内に返信
-
Multiple instructions show as one comma-joined paragraph ("questions., Conditional")対応中かも @Sanilas-ai が 7 日前に担当しました。 オープンArea: Shared Bug Difficulty: Low Good First Issue Priority: Low
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
DouglasNeuroInformatics/OpenDataCapture#1812 · コメント 1 件 ·
メンテナーはふだん 3 日以内に返信
-
Area: Instruments Bug Difficulty: Low Priority: Low
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
DouglasNeuroInformatics/OpenDataCapture#1801 · コメント 1 件 ·
メンテナーはふだん 3 日以内に返信
DouglasNeuroInformatics/OpenDataCapture の issue をすべて見る
似ている issue
-
content triage
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
tensorflow/tfjs#8754 ·
-
tool-submission
難易度 1/5 1時間未満 初心者へのやさしさ 83/100
BraveOPotato/NoSignups#2348 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
code-yeongyu/oh-my-openagent#12784 ·
メンテナーはふだん 1 日以内に返信
-
external-issue to-triage
難易度 2/5 1〜3時間 初心者へのやさしさ 71/100
メンテナーはふだん 1 日以内に返信