Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
336 changes: 168 additions & 168 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@next2d/player",
"version": "3.2.1",
"description": "Experience the fast and beautiful anti-aliased rendering of WebGL. You can create rich, interactive graphics, cross-platform applications and games without worrying about browser or device compatibility.",
"version": "3.3.0",
"description": "Experience the fast and beautiful anti-aliased rendering of WebGL/WebGPU. You can create rich, interactive graphics, cross-platform applications and games without worrying about browser or device compatibility.",
"author": "Toshiyuki Ienaga<ienaga@next2d.app> (https://github.com/ienaga/)",
"license": "MIT",
"homepage": "https://next2d.app",
Expand Down Expand Up @@ -55,14 +55,14 @@
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-terser": "^1.0.0",
"@rollup/plugin-typescript": "^12.3.0",
"@typescript-eslint/eslint-plugin": "^8.59.1",
"@typescript-eslint/parser": "^8.59.1",
"@typescript-eslint/eslint-plugin": "^8.59.2",
"@typescript-eslint/parser": "^8.59.2",
"@webgpu/types": "^0.1.69",
"eslint": "^10.3.0",
"eslint-plugin-unused-imports": "^4.4.1",
"globals": "^17.6.0",
"jsdom": "^29.1.1",
"rollup": "^4.60.2",
"rollup": "^4.60.3",
"typescript": "^6.0.3",
"vite": "^8.0.10",
"vitest": "^4.1.5",
Expand Down
78 changes: 78 additions & 0 deletions packages/cache/src/CacheStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { CacheStore } from "./CacheStore";
import { describe, expect, it, vi } from "vitest";

describe("CacheStore trash flow integration", () =>
{
it("removeTimer -> get -> removeTimerScheduledCache should still delete the entry", () =>
{
vi.useFakeTimers();

const store = new CacheStore();
store.set("12345", "0", { "value": "old" });

expect(store.has("12345", "0")).toBe(true);

// removeTimer で削除予定に登録
store.removeTimer("12345");
expect((store as any)["_$trash"].has("12345")).toBe(true);

// タイマー発火前に get が呼ばれても、削除予定は維持されない=
// 削除予定をキャンセルし、再度キャッシュとして有効化する
const value = store.get("12345", "0");
expect(value).toEqual({ "value": "old" });
expect((store as any)["_$trash"].has("12345")).toBe(false);

// タイマー発火 (1 秒経過)
vi.advanceTimersByTime(1000);
expect(store.$removeCache).toBe(true);

// get でキャンセルされたので removeTimerScheduledCache は何も削除しない
store.removeTimerScheduledCache();
expect(store.has("12345", "0")).toBe(true);
expect(store.$removeIds.length).toBe(0);

vi.useRealTimers();
});

it("removeTimer -> (no get) -> removeTimerScheduledCache should delete the entry and push removeIds", () =>
{
vi.useFakeTimers();

const store = new CacheStore();
store.$removeIds.length = 0;
store.set("67890", "0", { "value": "stale" });

store.removeTimer("67890");
expect((store as any)["_$trash"].has("67890")).toBe(true);

vi.advanceTimersByTime(1000);
expect(store.$removeCache).toBe(true);

store.removeTimerScheduledCache();

// 実際に削除されている
expect(store.has("67890", "0")).toBe(false);
expect(store.$removeIds).toContain(67890);
expect((store as any)["_$trash"].size).toBe(0);

vi.useRealTimers();
});

it("get on an entry not in trash_store should not affect trash_store of other entries", () =>
{
vi.useFakeTimers();

const store = new CacheStore();
store.set("aaa", "0", "alive");
store.set("bbb", "0", "doomed");

store.removeTimer("bbb");
expect((store as any)["_$trash"].has("bbb")).toBe(true);

// 別エントリへの get は他エントリの削除予定に影響を与えない
expect(store.get("aaa", "0")).toBe("alive");
expect((store as any)["_$trash"].has("bbb")).toBe(true);

vi.useRealTimers();
});
});
17 changes: 16 additions & 1 deletion packages/cache/src/CacheStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { execute as cacheStoreGenerateKeysService } from "./CacheStore/service/C
import { execute as cacheStoreGenerateFilterKeysService } from "./CacheStore/service/CacheStoreGenerateFilterKeysService";
import { execute as cacheStoreRemoveTimerService } from "./CacheStore/service/CacheStoreRemoveTimerService";
import { execute as cacheStoreRemoveTimerScheduledCacheService } from "./CacheStore/service/CacheStoreRemoveTimerScheduledCacheService";
import { execute as cacheStoreCancelRemoveTimerService } from "./CacheStore/service/CacheStoreCancelRemoveTimerService";

/**
* @description キャッシュ管理クラス
Expand Down Expand Up @@ -156,6 +157,20 @@ export class CacheStore
cacheStoreRemoveTimerService(this, this._$store, this._$trash, id);
}

/**
* @description 指定IDの削除タイマー登録を取り消す(再利用時に呼ぶ)
* Cancel the deletion timer registration for the specified ID (call on reuse)
*
* @param {string} id
* @returns {void}
* @method
* @public
*/
cancelRemoveTimer (id: string): void
{
cacheStoreCancelRemoveTimerService(this._$store, this._$trash, id);
}

/**
* @description タイマーでセットされた削除フラグを持つIDをキャッシュストアから削除する
* Remove the ID with the deletion flag set by the timer from the cache store
Expand Down Expand Up @@ -209,7 +224,7 @@ export class CacheStore
*/
get (unique_key: string, key: string): any
{
return cacheStoreGetService(this._$store, unique_key, key);
return cacheStoreGetService(this._$store, this._$trash, unique_key, key);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { execute } from "./CacheStoreCancelRemoveTimerService";
import { describe, expect, it } from "vitest";

describe("CacheStoreCancelRemoveTimerService.js test", () =>
{
it("test case1: trash_storeにidが存在しない場合は何もしない", () =>
{
const data = new Map();
const store = new Map();
store.set("2", data);
const trash = new Map();

execute(store, trash, "2");

expect(trash.size).toBe(0);
expect(data.has("trash")).toBe(false);
});

it("test case2: trash_storeにidが存在する場合はtrash_storeから削除し、data_storeのtrashキーも削除する", () =>
{
const data = new Map();
data.set("trash", true);

const store = new Map();
store.set("2", data);

const trash = new Map();
trash.set("2", data);

expect(trash.size).toBe(1);
expect(data.has("trash")).toBe(true);

execute(store, trash, "2");

expect(trash.size).toBe(0);
expect(data.has("trash")).toBe(false);
});

it("test case3: trash_storeにidが存在するがdata_storeにidが存在しない場合はtrash_storeのみ削除する", () =>
{
const store = new Map();
const trash = new Map();
trash.set("2", new Map());

expect(trash.size).toBe(1);

execute(store, trash, "2");

expect(trash.size).toBe(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @description 指定IDの削除タイマー登録を取り消す
* Cancel the deletion timer registration for the specified ID
*
* 削除タイマー(trash_store)に登録済みのエントリを再利用する場合に呼ぶ。
* これにより、1秒後の removeTimerScheduledCache() による wipe を防ぐ。
* 主にステージから一旦外れたインスタンスが addedToStage で再復帰した際の
* キャッシュ復活経路として使用する。
* When reusing an entry that has been registered for removal in the trash_store,
* this is called to prevent the wipe by removeTimerScheduledCache() after 1 second.
*
* @param {Map} data_store
* @param {Map} trash_store
* @param {string} id
* @return {void}
* @method
* @public
*/
export const execute = (
data_store: Map<string, Map<string, any>>,
trash_store: Map<string, Map<string, any>>,
id: string
): void => {

if (!trash_store.has(id)) {
return ;
}

trash_store.delete(id);

const data = data_store.get(id);
if (data) {
data.delete("trash");
}
};
59 changes: 56 additions & 3 deletions packages/cache/src/CacheStore/service/CacheStoreGetService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ describe("CacheStoreGetService.js test", () =>
it("test case1", () =>
{
const store = new Map();
expect(execute(store, "1", "0")).toBe(null);
const trash = new Map();
expect(execute(store, trash, "1", "0")).toBe(null);
});

it("test case2", () =>
{
const store = new Map();
const trash = new Map();
store.set("1", new Map());
expect(execute(store, "1", "0")).toBe(null);
expect(execute(store, trash, "1", "0")).toBe(null);
});

it("test case3", () =>
Expand All @@ -22,7 +24,58 @@ describe("CacheStoreGetService.js test", () =>
data.set("0", "test");

const store = new Map();
const trash = new Map();
store.set("1", data);
expect(execute(store, "1", "0")).toBe("test");
expect(execute(store, trash, "1", "0")).toBe("test");
});

it("get should cancel pending deletion when entry is in trash_store", () =>
{
const data = new Map();
data.set("0", "test");
data.set("trash", true);

const store = new Map();
store.set("1", data);

const trash = new Map();
trash.set("1", data);

expect(trash.has("1")).toBe(true);
expect(data.has("trash")).toBe(true);

const result = execute(store, trash, "1", "0");

// 値は問題なく取得できる
expect(result).toBe("test");

// 削除予定はキャンセルされる
expect(trash.has("1")).toBe(false);
expect(data.has("trash")).toBe(false);

// 元データは削除されていない
expect(store.has("1")).toBe(true);
expect(data.get("0")).toBe("test");
});

it("get should not touch trash_store for entries that are not pending deletion", () =>
{
const data = new Map();
data.set("0", "test");

const otherData = new Map();
otherData.set("trash", true);

const store = new Map();
store.set("1", data);

const trash = new Map();
trash.set("2", otherData);

// 別 id のエントリへの get は trash_store を触らない
execute(store, trash, "1", "0");

expect(trash.has("2")).toBe(true);
expect(otherData.has("trash")).toBe(true);
});
});
22 changes: 18 additions & 4 deletions packages/cache/src/CacheStore/service/CacheStoreGetService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@
* @description 指定のキーからデータを取得
* Get data from the specified key
*
* 削除予定(trash_store 登録済み)のエントリは get 時に削除予定をキャンセルする。
* これにより、get 後の removeTimerScheduledCache() で
* 内側 Map の "trash" マーク削除によりループがスキップされる問題を回避する。
* When entries are pending deletion (registered in trash_store), the
* scheduled removal is canceled here. This prevents the
* removeTimerScheduledCache() loop from skipping such entries due to
* the inner Map's "trash" marker having been deleted.
*
* @param {Map} data_store
* @param {Map} trash_store
* @param {string} unique_key
* @param {string} key
* @return {*}
* @method
* @public
*/
* @public
*/
export const execute = (
data_store: Map<string, Map<string, any>>,
trash_store: Map<string, Map<string, any>>,
unique_key: string,
key: string
): any => {
Expand All @@ -20,6 +30,10 @@ export const execute = (
return null;
}

data.delete("trash");
if (trash_store.has(unique_key)) {
trash_store.delete(unique_key);
data.delete("trash");
}

return data.get(key) || null;
};
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DisplayObject } from "../../DisplayObject";
import { Event } from "@next2d/events";
import { $stageAssignedMap } from "../../DisplayObjectUtil";
import { $cacheStore } from "@next2d/cache";

/**
* @description DisplayObjectのADDED_TO_STAGEイベントを実行
Expand All @@ -19,6 +20,14 @@ export const execute = <D extends DisplayObject>(display_object: D): void =>
return ;
}

// 一旦ステージから外れて削除タイマーに乗ったインスタンスがそのまま再復帰した場合、
// 1秒後の wipe で巻き添えにならないよう trash 登録を取り消す。
// ナビゲーション(毎回新インスタンス生成)では trash に該当 id がないため no-op。
if (display_object.uniqueKey) {
$cacheStore.cancelRemoveTimer(display_object.uniqueKey);
}
$cacheStore.cancelRemoveTimer(`${display_object.instanceId}`);

display_object.$addedToStage = true;
if (display_object.willTrigger(Event.ADDED_TO_STAGE)) {
display_object.dispatchEvent(new Event(Event.ADDED_TO_STAGE));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,16 @@ export const execute = <D extends DisplayObject>(display_object: D): void =>
) {
$cacheStore.removeTimer(display_object.uniqueKey);
}

// instanceId ベースキャッシュの cleanup
// - コンテナの filter/cacheAsBitmap/blend (Main に "filterKey"/"bitmapKey" あり) → removeTimer (1秒猶予で再復帰可)
// - Shape/Text/Video の filter (ContextApplyFilterUseCase が Worker 側 "fKey"/"fTexture"/"offsetX"/"offsetY" を格納)
// は Main 側にエントリがないため、$removeIds に直接 push して Worker 側の GPU リソースを解放する。
// どちらも放置するとナビゲーション繰り返しでアトラス/GPU メモリが枯渇する。
const instanceIdKey = `${display_object.instanceId}`;
if ($cacheStore.has(instanceIdKey)) {
$cacheStore.removeTimer(instanceIdKey);
} else {
$cacheStore.$removeIds.push(display_object.instanceId);
}
};
Loading
Loading