Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
.user.json
.DS_Store
.temp
.runtime-bundle-cache*

# tsconfig
tsconfig.tsbuildinfo
Expand Down
11 changes: 8 additions & 3 deletions packages/asset-db/source/libs/filesystem/local-provider.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
'use strict';

import { dirname } from 'path';
import { copy, ensureDir, existsSync, move, outputFile, readFile, remove, stat } from 'fs-extra';
import { access, copy, ensureDir, move, outputFile, readFile, remove, stat } from 'fs-extra';
import { IAssetDeleteOptions, IAssetFileSystemProvider, IAssetRenameOptions, IAssetWriteFileOptions } from './provider';

export class LocalAssetFileSystemProvider implements IAssetFileSystemProvider {
exists(path: string) {
return existsSync(path);
async exists(path: string) {
try {
await access(path);
return true;
} catch {
return false;
}
}

async stat(path: string) {
Expand Down
17 changes: 17 additions & 0 deletions packages/asset-db/test/16.filesystem-provider.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const fse = require('fs-extra');
const path = require('path');

const assetdb = require('../dist');
const { LocalAssetFileSystemProvider } = require('../dist/libs/filesystem/local-provider');
const {
fsCopy,
fsReadFile,
Expand Down Expand Up @@ -44,6 +45,22 @@ describe('AssetDB 文件系统 Provider', () => {
return { db, asset };
}

it('LocalAssetFileSystemProvider.exists 使用异步 access 检查路径', async () => {
const provider = new LocalAssetFileSystemProvider();
const existingPath = path.join(PATH.ROOT, 'exists.txt');
const missingPath = path.join(PATH.ROOT, 'missing.txt');

fse.outputFileSync(existingPath, 'exists');

const existingResult = provider.exists(existingPath);
const missingResult = provider.exists(missingPath);

expect(existingResult).to.be.instanceOf(Promise);
expect(missingResult).to.be.instanceOf(Promise);
expect(await existingResult).to.equal(true);
expect(await missingResult).to.equal(false);
});

afterEach(() => {
if (typeof assetdb.resetFileSystemProvider === 'function') {
assetdb.resetFileSystemProvider();
Expand Down
26 changes: 19 additions & 7 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import '../lib/runtime-module-cache';
import type { EngineApi } from '../api/engine/engine';
import type { ProjectApi } from '../api/project/project';
import type { AssetsApi } from '../api/assets/assets';
Expand Down Expand Up @@ -32,19 +33,30 @@ export class CocosAPI {
* 初始化 API 实例,主要是为了实现按需加载
*/
private async _init() {
const { SceneApi } = await import('../api/scene/scene');
// 各模块之间无实例级依赖,可并行加载(模块加载器保证共享依赖只求值一次)
const [
{ SceneApi },
{ EngineApi },
{ ProjectApi },
{ AssetsApi },
{ BuilderApi },
{ ConfigurationApi },
{ SystemApi },
] = await Promise.all([
import('../api/scene/scene'),
import('../api/engine/engine'),
import('../api/project/project'),
import('../api/assets/assets'),
import('../api/builder/builder'),
import('../api/configuration/configuration'),
import('../api/system/system'),
]);
this.scene = new SceneApi();
const { EngineApi } = await import('../api/engine/engine');
this.engine = new EngineApi();
const { ProjectApi } = await import('../api/project/project');
this.project = new ProjectApi();
const { AssetsApi } = await import('../api/assets/assets');
this.assets = new AssetsApi();
const { BuilderApi } = await import('../api/builder/builder');
this.builder = new BuilderApi();
const { ConfigurationApi } = await import('../api/configuration/configuration');
this.configuration = new ConfigurationApi();
const { SystemApi } = await import('../api/system/system');
this.system = new SystemApi();
}

Expand Down
62 changes: 40 additions & 22 deletions src/core/assets/manager/asset-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ class AssetDBManager extends EventEmitter {
private state: RefreshState = 'free';
public assetDBInfo: Record<string, IAssetDBInfo> = {};
private waitingTaskQueue: IWaitingTaskInfo[] = [];
private waitingRefreshAsset: string[] = [];
private pendingAutoRefreshResolves: Function[] = [];
private autoRefreshTimer?: NodeJS.Timeout;
private waringRefreshAsset: string[] = [];
private autoRefreshAssetLazyPending = false;
private waringRefreshAssetPendingMap = new Map<string, Function[]>();
private get assetBusy() {
return this.assetBusyTask.size > 0;
}
Expand Down Expand Up @@ -492,26 +492,44 @@ class AssetDBManager extends EventEmitter {
* 懒刷新资源,请勿使用,目前的逻辑是针对重刷文件夹定制的
* @param file
*/
public async autoRefreshAssetLazy(pathOrUrlOrUUID: string) {
if (!this.waitingRefreshAsset.includes(pathOrUrlOrUUID)) {
this.waitingRefreshAsset.push(pathOrUrlOrUUID);
}

this.autoRefreshTimer && clearTimeout(this.autoRefreshTimer);
return new Promise((resolve) => {
this.pendingAutoRefreshResolves.push(resolve);
this.autoRefreshTimer = setTimeout(async () => {
const taskId = 'autoRefreshAssetLazy' + Date.now();
this.assetBusyTask.add(taskId);
const files = JSON.parse(JSON.stringify(this.waitingRefreshAsset));
this.waitingRefreshAsset.length = 0;
await Promise.all(files.map((file: string) => assetdb.refresh(file)));
this.assetBusyTask.delete(taskId);
this.step();
this.pendingAutoRefreshResolves.forEach((resolve) => resolve(true));
this.pendingAutoRefreshResolves.length = 0;
}, 100);
public autoRefreshAssetLazy(pathOrUrlOrUUID: string): Promise<boolean> {
if (!this.waringRefreshAsset.includes(pathOrUrlOrUUID)) {
this.waringRefreshAsset.push(pathOrUrlOrUUID);
}

const promise = new Promise<boolean>((resolve) => {
const pending = this.waringRefreshAssetPendingMap.get(pathOrUrlOrUUID) || [];
pending.push(resolve);
this.waringRefreshAssetPendingMap.set(pathOrUrlOrUUID, pending);
});

if (this.autoRefreshAssetLazyPending) {
return promise;
}

this.autoRefreshAssetLazyPending = true;
void (async () => {
try {
while (this.waringRefreshAsset.length > 0) {
const files = Array.from(this.waringRefreshAsset);
this.waringRefreshAsset.length = 0;
const taskId = 'autoRefreshAssetLazy' + Date.now();
this.assetBusyTask.add(taskId);
await Promise.all(files.map((file) => assetdb.refresh(file)));
this.assetBusyTask.delete(taskId);
this.step();

files.forEach((file) => {
const pending = this.waringRefreshAssetPendingMap.get(file);
pending?.forEach((resolve) => resolve(true));
this.waringRefreshAssetPendingMap.delete(file);
});
}
} finally {
this.autoRefreshAssetLazyPending = false;
}
})();
return promise;
}

/**
Expand Down
55 changes: 55 additions & 0 deletions src/core/assets/test/auto-refresh-asset-lazy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as assetdb from '@cocos/asset-db';
import assetDBManager from '../manager/asset-db';

describe('AssetDBManager.autoRefreshAssetLazy', () => {
const manager = assetDBManager as any;

beforeEach(() => {
manager.assetBusyTask.clear();
manager.waringRefreshAsset.length = 0;
manager.autoRefreshAssetLazyPending = false;
manager.waringRefreshAssetPendingMap.clear();
jest.spyOn(manager, 'step').mockResolvedValue(undefined);
});

afterEach(() => {
jest.restoreAllMocks();
});

it('starts immediately and resolves queued calls after each refresh', async () => {
const releases: Record<string, () => void> = {};
let second!: Promise<boolean>;
let third!: Promise<boolean>;
jest.spyOn(assetdb, 'refresh').mockImplementation(async (file) => {
if (file === 'first') {
second = manager.autoRefreshAssetLazy('second');
third = manager.autoRefreshAssetLazy('third');
}
await new Promise<void>((resolve) => {
releases[file] = resolve;
});
return 0;
});
const refresh = assetdb.refresh as jest.Mock;

const first = manager.autoRefreshAssetLazy('first');
await Promise.resolve();
expect(refresh).toHaveBeenCalledWith('first');

releases.first();
await new Promise<void>((resolve) => setImmediate(resolve));
expect(refresh.mock.calls.map(([file]) => file)).toEqual(['first', 'second', 'third']);

releases.second();
await new Promise<void>((resolve) => setImmediate(resolve));
let secondResolved = false;
void second.then(() => { secondResolved = true; });
expect(secondResolved).toBe(false);

releases.third();
await expect(second).resolves.toBe(true);
await expect(third).resolves.toBe(true);
await expect(first).resolves.toBe(true);
expect(refresh).toHaveBeenCalledTimes(3);
});
});
12 changes: 8 additions & 4 deletions src/core/scene/scene-process/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import '../../../lib/runtime-module-cache';
import { SceneReadyChannel } from '../common';
import { Rpc } from './rpc';
import { parseCommandLineArgs, resolveSceneAssetBase } from './utils';
import { Engine } from '../../engine';
import { join } from 'path';
import { serviceManager } from './service/service-manager';
import { installSceneEditorShim } from './editor-shim';
import { StartupCpuProfiler } from './startup-cpu-profiler';

async function startup() {
// 监听进程退出事件
Expand Down Expand Up @@ -69,7 +71,9 @@ async function startup() {
console.log(`[Scene] startup worker success, cocos version: ${cc.ENGINE_VERSION}`);
}

startup().catch(err => {
console.error('[Scene] Startup fatal error:', err);
process.exit(1);
});
new StartupCpuProfiler('VSCODE_COCOS_SCENE_PROCESS_CPU_PROFILE')
.run(startup)
.catch(err => {
console.error('[Scene] Startup fatal error:', err);
process.exit(1);
});
63 changes: 63 additions & 0 deletions src/core/scene/scene-process/startup-cpu-profiler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Session } from 'inspector';
import * as fs from 'fs';
import { StartupCpuProfiler } from './startup-cpu-profiler';

jest.mock('inspector', () => ({
Session: jest.fn().mockImplementation(() => ({
connect: jest.fn(),
disconnect: jest.fn(),
post: jest.fn((method: string, callback: (error: Error | null, params?: object) => void) => {
if (method === 'Profiler.stop') {
callback(null, { profile: { nodes: [] } });
} else {
callback(null);
}
}),
})),
}));

jest.mock('fs', () => ({
writeFileSync: jest.fn(),
}));

const profileEnvKey = 'VSCODE_COCOS_SCENE_PROCESS_CPU_PROFILE';
const originalProfilePath = process.env[profileEnvKey];

afterEach(() => {
jest.clearAllMocks();
if (originalProfilePath === undefined) {
delete process.env[profileEnvKey];
} else {
process.env[profileEnvKey] = originalProfilePath;
}
});

it('runs transparently when the environment variable is not a .cpuprofile path', async () => {
delete process.env[profileEnvKey];
const task = jest.fn(async () => 'ready');

const result = await new StartupCpuProfiler(profileEnvKey).run(task);

expect(result).toBe('ready');
expect(task).toHaveBeenCalledTimes(1);
expect(Session).not.toHaveBeenCalled();
});

it('records startup and writes the profile to the configured path', async () => {
const outputPath = '/tmp/scene-process.cpuprofile';
process.env[profileEnvKey] = outputPath;
const task = jest.fn(async () => 'ready');

const result = await new StartupCpuProfiler(profileEnvKey).run(task);

expect(result).toBe('ready');
expect(task).toHaveBeenCalledTimes(1);
expect(Session).toHaveBeenCalledTimes(1);
const session = (Session as unknown as jest.Mock).mock.results[0].value;
expect(session.connect).toHaveBeenCalledTimes(1);
expect(session.post).toHaveBeenNthCalledWith(1, 'Profiler.enable', expect.any(Function));
expect(session.post).toHaveBeenNthCalledWith(2, 'Profiler.start', expect.any(Function));
expect(session.post).toHaveBeenNthCalledWith(3, 'Profiler.stop', expect.any(Function));
expect(fs.writeFileSync).toHaveBeenCalledWith(outputPath, JSON.stringify({ nodes: [] }));
expect(session.disconnect).toHaveBeenCalledTimes(1);
});
Loading
Loading