Skip to content

Commit

Permalink
Merge pull request #2019 from cardstack/debug-perf-logging
Browse files Browse the repository at this point in the history
Optimize logic around invalidation
  • Loading branch information
habdelra authored Jan 9, 2025
2 parents c646313 + e8a615e commit 73c9da3
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 57 deletions.
43 changes: 27 additions & 16 deletions packages/host/app/components/card-prerender.gts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,15 @@ export default class CardPrerender extends Component {
}
}

private async fromScratch(realmURL: URL): Promise<IndexResults> {
private async fromScratch(
realmURL: URL,
invalidateEntireRealm: boolean,
): Promise<IndexResults> {
try {
let results = await this.doFromScratch.perform(realmURL);
let results = await this.doFromScratch.perform(
realmURL,
invalidateEntireRealm,
);
return results;
} catch (e: any) {
if (!didCancel(e)) {
Expand Down Expand Up @@ -99,21 +105,26 @@ export default class CardPrerender extends Component {
await register(this.fromScratch.bind(this), this.incremental.bind(this));
});

private doFromScratch = enqueueTask(async (realmURL: URL) => {
let { reader, indexWriter } = this.getRunnerParams(realmURL);
let currentRun = new CurrentRun({
realmURL,
reader,
indexWriter,
renderCard: this.renderService.renderCard,
render: this.renderService.render,
});
setOwner(currentRun, getOwner(this)!);
private doFromScratch = enqueueTask(
async (realmURL: URL, invalidateEntireRealm: boolean) => {
let { reader, indexWriter } = this.getRunnerParams(realmURL);
let currentRun = new CurrentRun({
realmURL,
reader,
indexWriter,
renderCard: this.renderService.renderCard,
render: this.renderService.render,
});
setOwner(currentRun, getOwner(this)!);

let current = await CurrentRun.fromScratch(currentRun);
this.renderService.indexRunDeferred?.fulfill();
return current;
});
let current = await CurrentRun.fromScratch(
currentRun,
invalidateEntireRealm,
);
this.renderService.indexRunDeferred?.fulfill();
return current;
},
);

private doIncremental = enqueueTask(
async (
Expand Down
87 changes: 79 additions & 8 deletions packages/host/app/lib/current-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import type LoaderService from '../services/loader-service';
import type NetworkService from '../services/network';

const log = logger('current-run');
const perfLog = logger('index-perf');

interface CardType {
refURL: string;
Expand Down Expand Up @@ -121,24 +122,69 @@ export class CurrentRun {
this.#render = render;
}

static async fromScratch(current: CurrentRun): Promise<IndexResults> {
static async fromScratch(
current: CurrentRun,
invalidateEntireRealm?: boolean,
): Promise<IndexResults> {
let start = Date.now();
log.debug(`starting from scratch indexing`);
perfLog.debug(
`starting from scratch indexing for realm ${current.realmURL.href}`,
);

current.#batch = await current.#indexWriter.createBatch(current.realmURL);
let mtimes = await current.batch.getModifiedTimes();
await current.discoverInvalidations(current.realmURL, mtimes);
let invalidations = current.batch.invalidations.map(
(href) => new URL(href),
);
let invalidations: URL[] = [];
if (invalidateEntireRealm) {
perfLog.debug(
`flag was set to invalidate entire realm ${current.realmURL.href}, skipping invalidation discovery`,
);
let mtimesStart = Date.now();
let filesystemMtimes = await current.#reader.mtimes();
perfLog.debug(
`time to get file system mtimes ${Date.now() - mtimesStart} ms`,
);
invalidations = Object.keys(filesystemMtimes)
.filter(
(url) =>
// Only allow json and executable files to be invalidated so that we
// don't end up with invalidated files that weren't meant to be indexed
// (images, etc)
url.endsWith('.json') || hasExecutableExtension(url),
)
.map((url) => new URL(url));
} else {
let mtimesStart = Date.now();
let mtimes = await current.batch.getModifiedTimes();
perfLog.debug(
`completed getting index mtimes in ${Date.now() - mtimesStart} ms`,
);
let invalidateStart = Date.now();
invalidations = (
await current.discoverInvalidations(current.realmURL, mtimes)
).map((href) => new URL(href));
perfLog.debug(
`completed invalidations in ${Date.now() - invalidateStart} ms`,
);
}

await current.whileIndexing(async () => {
let visitStart = Date.now();
for (let invalidation of invalidations) {
await current.tryToVisit(invalidation);
}
perfLog.debug(`completed index visit in ${Date.now() - visitStart} ms`);
let finalizeStart = Date.now();
let { totalIndexEntries } = await current.batch.done();
perfLog.debug(
`completed index finalization in ${Date.now() - finalizeStart} ms`,
);
current.stats.totalIndexEntries = totalIndexEntries;
log.debug(`completed from scratch indexing in ${Date.now() - start}ms`);
perfLog.debug(
`completed from scratch indexing for realm ${
current.realmURL.href
} in ${Date.now() - start} ms`,
);
});
return {
invalidations: [...(invalidations ?? [])].map((url) => url.href),
Expand Down Expand Up @@ -235,17 +281,26 @@ export class CurrentRun {
private async discoverInvalidations(
url: URL,
indexMtimes: LastModifiedTimes,
): Promise<void> {
): Promise<string[]> {
log.debug(`discovering invalidations in dir ${url.href}`);
perfLog.debug(`discovering invalidations in dir ${url.href}`);
let ignoreStart = Date.now();
let ignorePatterns = await this.#reader.readFile(
new URL('.gitignore', url),
);
perfLog.debug(`time to get ignore rules ${Date.now() - ignoreStart} ms`);
if (ignorePatterns && ignorePatterns.content) {
this.ignoreMap.set(url.href, ignore().add(ignorePatterns.content));
this.#ignoreData[url.href] = ignorePatterns.content;
}

let mtimesStart = Date.now();
let filesystemMtimes = await this.#reader.mtimes();
perfLog.debug(
`time to get file system mtimes ${Date.now() - mtimesStart} ms`,
);
let invalidationList: string[] = [];
let skipList: string[] = [];
for (let [url, lastModified] of Object.entries(filesystemMtimes)) {
if (!url.endsWith('.json') && !hasExecutableExtension(url)) {
// Only allow json and executable files to be invalidated so that we
Expand All @@ -261,9 +316,25 @@ export class CurrentRun {
indexEntry.lastModified == null ||
lastModified !== indexEntry.lastModified
) {
await this.batch.invalidate(new URL(url));
invalidationList.push(url);
} else {
skipList.push(url);
}
}
if (skipList.length === 0) {
// the whole realm needs to be visited, no need to calculate
// invalidations--it's everything
return invalidationList;
}

let invalidationStart = Date.now();
for (let invalidationURL of invalidationList) {
await this.batch.invalidate(new URL(invalidationURL));
}
perfLog.debug(
`time to invalidate ${url} ${Date.now() - invalidationStart} ms`,
);
return this.batch.invalidations;
}

private async visitFile(
Expand Down
5 changes: 4 additions & 1 deletion packages/host/app/services/local-indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { type TestRealmAdapter } from '@cardstack/host/tests/helpers/adapter';
// for the test-realm-adapter
export default class LocalIndexer extends Service {
setup(
_fromScratch: (realmURL: URL) => Promise<IndexResults>,
_fromScratch: (
realmURL: URL,
invalidateEntireRealm: boolean,
) => Promise<IndexResults>,
_incremental: (
url: URL,
realmURL: URL,
Expand Down
2 changes: 1 addition & 1 deletion packages/realm-server/handlers/handle-create-realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export default function handleCreateRealmRequest({
}

let creationTimeMs = Date.now() - start;
if (creationTimeMs > 15_000) {
if (creationTimeMs > 30_000) {
let msg = `it took a long time, ${creationTimeMs} ms, to create realm for ${ownerUserId}, ${JSON.stringify(
json.data.attributes,
)}`;
Expand Down
25 changes: 14 additions & 11 deletions packages/realm-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,18 +347,21 @@ export class RealmServer {
this.log.debug(`seed files for new realm ${url} copied to ${realmPath}`);
}

let realm = new Realm({
url,
adapter,
secretSeed: this.secretSeed,
virtualNetwork: this.virtualNetwork,
dbAdapter: this.dbAdapter,
queue: this.queue,
matrix: {
url: this.matrixClient.matrixURL,
username,
let realm = new Realm(
{
url,
adapter,
secretSeed: this.secretSeed,
virtualNetwork: this.virtualNetwork,
dbAdapter: this.dbAdapter,
queue: this.queue,
matrix: {
url: this.matrixClient.matrixURL,
username,
},
},
});
{ invalidateEntireRealm: true },
);
this.realms.push(realm);
this.virtualNetwork.mount(realm.handle);
return realm;
Expand Down
27 changes: 16 additions & 11 deletions packages/runtime-common/index-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
trimExecutableExtension,
RealmPaths,
unixTime,
logger,
} from './index';
import { transpileJS } from './transpile';
import {
Expand Down Expand Up @@ -97,6 +98,7 @@ export class Batch {
readonly ready: Promise<void>;
#invalidations = new Set<string>();
#dbAdapter: DBAdapter;
#perfLog = logger('index-perf');
private declare realmVersion: number;

constructor(
Expand Down Expand Up @@ -464,6 +466,7 @@ export class Batch {
): Promise<
{ url: string; alias: string; type: 'instance' | 'module' | 'error' }[]
> {
let start = Date.now();
const pageSize = 1000;
let results: (Pick<BoxelIndexTable, 'url' | 'file_alias'> & {
type: 'instance' | 'module' | 'error';
Expand Down Expand Up @@ -495,6 +498,9 @@ export class Batch {
// css is a subset of modules, so there won't by any references that
// are css entries that aren't already represented by a module entry
[`i.type != 'css'`],
// probably need to reevaluate this condition when we get to cross
// realm invalidation
[`i.realm_url =`, param(this.realmURL.href)],
]),
'ORDER BY i.url COLLATE "POSIX"',
`LIMIT ${pageSize} OFFSET ${pageNumber * pageSize}`,
Expand All @@ -504,6 +510,11 @@ export class Batch {
results = [...results, ...rows];
pageNumber++;
} while (rows.length === pageSize);
this.#perfLog.debug(
`time to determine items that reference ${resolvedPath} ${
Date.now() - start
} ms`,
);
return results.map(({ url, file_alias, type }) => ({
url,
alias: file_alias,
Expand All @@ -522,17 +533,11 @@ export class Batch {
return [];
}
visited.add(resolvedPath);
let childInvalidations = await this.itemsThatReference(resolvedPath);
let realmPath = new RealmPaths(this.realmURL);
let invalidationsInThisRealm = childInvalidations.filter((c) =>
realmPath.inRealm(new URL(c.url)),
);

let invalidations = invalidationsInThisRealm.map(({ url }) => url);
let aliases = invalidationsInThisRealm.map(
({ alias: moduleAlias, type, url }) =>
// for instances we expect that the deps for an entry always includes .json extension
type === 'instance' ? url : moduleAlias,
let items = await this.itemsThatReference(resolvedPath);
let invalidations = items.map(({ url }) => url);
let aliases = items.map(({ alias: moduleAlias, type, url }) =>
// for instances we expect that the deps for an entry always includes .json extension
type === 'instance' ? url : moduleAlias,
);
let results = [
...invalidations,
Expand Down
11 changes: 6 additions & 5 deletions packages/runtime-common/realm-index-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
type Stats,
type DBAdapter,
type QueuePublisher,
type WorkerArgs,
type FromScratchArgs,
type FromScratchResult,
type IncrementalArgs,
type IncrementalResult,
Expand Down Expand Up @@ -81,8 +81,8 @@ export class RealmIndexUpdater {
return await this.#indexWriter.isNewIndex(this.realmURL);
}

async run() {
await this.fullIndex();
async run(invalidateEntireRealm?: boolean) {
await this.fullIndex(invalidateEntireRealm);
}

indexing() {
Expand All @@ -92,12 +92,13 @@ export class RealmIndexUpdater {
// TODO consider triggering SSE events for invalidations now that we can
// calculate fine grained invalidations for from-scratch indexing by passing
// in an onInvalidation callback
async fullIndex() {
async fullIndex(invalidateEntireRealm?: boolean) {
this.#indexingDeferred = new Deferred<void>();
try {
let args: WorkerArgs = {
let args: FromScratchArgs = {
realmURL: this.#realm.url,
realmUsername: await this.getRealmUsername(),
invalidateEntireRealm: Boolean(invalidateEntireRealm),
};
let job = await this.#queue.publish<FromScratchResult>(
`from-scratch-index`,
Expand Down
5 changes: 4 additions & 1 deletion packages/runtime-common/realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export interface RealmAdapter {

interface Options {
disableModuleCaching?: true;
invalidateEntireRealm?: true;
}

interface UpdateItem {
Expand Down Expand Up @@ -252,6 +253,7 @@ export class Realm {
#recentWrites: Map<string, number> = new Map();
#realmSecretSeed: string;
#disableModuleCaching = false;
#invalidateEntireRealm = false;

#publicEndpoints: RouteTable<true> = new Map([
[
Expand Down Expand Up @@ -305,6 +307,7 @@ export class Realm {
seed: secretSeed,
});
this.#disableModuleCaching = Boolean(opts?.disableModuleCaching);
this.#invalidateEntireRealm = Boolean(opts?.invalidateEntireRealm);

let fetch = fetcher(virtualNetwork.fetch, [
async (req, next) => {
Expand Down Expand Up @@ -591,7 +594,7 @@ export class Realm {
await Promise.resolve();
let startTime = Date.now();
let isNewIndex = await this.#realmIndexUpdater.isNewIndex();
let promise = this.#realmIndexUpdater.run();
let promise = this.#realmIndexUpdater.run(this.#invalidateEntireRealm);
if (isNewIndex) {
// we only await the full indexing at boot if this is a brand new index
await promise;
Expand Down
Loading

0 comments on commit 73c9da3

Please sign in to comment.