Revert "feat(search): try to decrease complexity"

This reverts commit 5b79e0d71e.
This commit is contained in:
perf3ct
2025-09-02 19:24:47 +00:00
parent 0afb8a11c8
commit 06b2d71b27
7 changed files with 1833 additions and 2064 deletions

View File

@@ -15,75 +15,6 @@ import * as path from 'path';
import * as fs from 'fs';
import { randomBytes } from 'crypto';
// Resource manager for proper cleanup
class ResourceManager {
private resources: Array<{ name: string; cleanup: () => void | Promise<void> }> = [];
private cleanedUp = false;
register(name: string, cleanup: () => void | Promise<void>): void {
console.log(`[ResourceManager] Registered resource: ${name}`);
this.resources.push({ name, cleanup });
}
async cleanup(): Promise<void> {
if (this.cleanedUp) {
console.log('[ResourceManager] Already cleaned up, skipping...');
return;
}
console.log('[ResourceManager] Starting cleanup...');
this.cleanedUp = true;
// Cleanup in reverse order of registration
for (let i = this.resources.length - 1; i >= 0; i--) {
const resource = this.resources[i];
try {
console.log(`[ResourceManager] Cleaning up: ${resource.name}`);
await resource.cleanup();
console.log(`[ResourceManager] Successfully cleaned up: ${resource.name}`);
} catch (error) {
console.error(`[ResourceManager] Error cleaning up ${resource.name}:`, error);
}
}
this.resources = [];
console.log('[ResourceManager] Cleanup completed');
}
}
// Global resource manager
const resourceManager = new ResourceManager();
// Setup process exit handlers
process.on('exit', (code) => {
console.log(`[Process] Exiting with code: ${code}`);
});
process.on('SIGINT', async () => {
console.log('\n[Process] Received SIGINT, cleaning up...');
await resourceManager.cleanup();
process.exit(130); // Standard exit code for SIGINT
});
process.on('SIGTERM', async () => {
console.log('\n[Process] Received SIGTERM, cleaning up...');
await resourceManager.cleanup();
process.exit(143); // Standard exit code for SIGTERM
});
process.on('uncaughtException', async (error) => {
console.error('[Process] Uncaught exception:', error);
await resourceManager.cleanup();
process.exit(1);
});
process.on('unhandledRejection', async (reason, promise) => {
console.error('[Process] Unhandled rejection at:', promise, 'reason:', reason);
await resourceManager.cleanup();
process.exit(1);
});
// Parse command line arguments
const noteCount = parseInt(process.argv[2]);
const batchSize = parseInt(process.argv[3]) || 100;
@@ -110,6 +41,15 @@ console.log(` Batch size: ${batchSize.toLocaleString()}`);
console.log(` Database: ${DB_PATH}`);
console.log(`============================================\n`);
// Open database
const db = new Database(DB_PATH);
// Enable optimizations
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
db.pragma('cache_size = 10000');
db.pragma('temp_store = MEMORY');
// Helper functions that mimic Trilium's ID generation
function newEntityId(prefix: string = ''): string {
return prefix + randomBytes(12).toString('base64').replace(/[+/=]/g, '').substring(0, 12);
@@ -185,18 +125,15 @@ function generateContent(): string {
}
// Native-style service functions
function createNote(
db: Database.Database,
params: {
noteId: string;
title: string;
content: string;
type: string;
mime?: string;
isProtected?: boolean;
parentNoteId?: string;
}
) {
function createNote(params: {
noteId: string;
title: string;
content: string;
type: string;
mime?: string;
isProtected?: boolean;
parentNoteId?: string;
}) {
const currentDateTime = utcNowDateTime();
const noteStmt = db.prepare(`
INSERT INTO notes (noteId, title, isProtected, type, mime, blobId, isDeleted, deleteId,
@@ -258,16 +195,13 @@ function createNote(
return params.noteId;
}
function createAttribute(
db: Database.Database,
params: {
noteId: string;
type: 'label' | 'relation';
name: string;
value: string;
isInheritable?: boolean;
}
) {
function createAttribute(params: {
noteId: string;
type: 'label' | 'relation';
name: string;
value: string;
isInheritable?: boolean;
}) {
const currentDateTime = utcNowDateTime();
const stmt = db.prepare(`
INSERT INTO attributes (attributeId, noteId, type, name, value, position,
@@ -289,212 +223,148 @@ function createAttribute(
);
}
async function main(): Promise<void> {
let db: Database.Database | null = null;
let exitCode = 0;
try {
const startTime = Date.now();
const allNoteIds: string[] = ['root'];
let notesCreated = 0;
let attributesCreated = 0;
console.log('Opening database connection...');
// Open database with proper error handling
try {
db = new Database(DB_PATH);
resourceManager.register('Database Connection', () => {
if (db && db.open) {
console.log('Closing database connection...');
db.close();
console.log('Database connection closed');
}
});
} catch (error) {
console.error('Failed to open database:', error);
throw error;
}
// Enable optimizations
console.log('Configuring database optimizations...');
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
db.pragma('cache_size = 10000');
db.pragma('temp_store = MEMORY');
console.log('Starting note generation...\n');
// Create container note
const containerNoteId = newEntityId();
const containerTransaction = db.transaction(() => {
createNote(db!, {
noteId: containerNoteId,
title: `Stress Test ${new Date().toISOString()}`,
content: `<p>Container for stress test with ${noteCount} notes</p>`,
type: 'text',
parentNoteId: 'root'
});
async function main() {
const startTime = Date.now();
const allNoteIds: string[] = ['root'];
let notesCreated = 0;
let attributesCreated = 0;
console.log('Starting note generation...\n');
// Create container note
const containerNoteId = newEntityId();
const containerTransaction = db.transaction(() => {
createNote({
noteId: containerNoteId,
title: `Stress Test ${new Date().toISOString()}`,
content: `<p>Container for stress test with ${noteCount} notes</p>`,
type: 'text',
parentNoteId: 'root'
});
});
containerTransaction();
console.log(`Created container note: ${containerNoteId}`);
allNoteIds.push(containerNoteId);
// Process in batches
for (let batch = 0; batch < Math.ceil(noteCount / batchSize); batch++) {
const batchStart = batch * batchSize;
const batchEnd = Math.min(batchStart + batchSize, noteCount);
const batchNoteCount = batchEnd - batchStart;
try {
containerTransaction();
console.log(`Created container note: ${containerNoteId}`);
allNoteIds.push(containerNoteId);
} catch (error) {
console.error('Failed to create container note:', error);
throw error;
}
// Process in batches
for (let batch = 0; batch < Math.ceil(noteCount / batchSize); batch++) {
const batchStart = batch * batchSize;
const batchEnd = Math.min(batchStart + batchSize, noteCount);
const batchNoteCount = batchEnd - batchStart;
const batchTransaction = db.transaction(() => {
for (let i = 0; i < batchNoteCount; i++) {
const noteId = newEntityId();
const type = noteTypes[Math.floor(Math.random() * noteTypes.length)];
const batchTransaction = db.transaction(() => {
for (let i = 0; i < batchNoteCount; i++) {
const noteId = newEntityId();
const type = noteTypes[Math.floor(Math.random() * noteTypes.length)];
// Decide parent - either container or random existing note
let parentNoteId = containerNoteId;
if (allNoteIds.length > 10 && Math.random() < 0.3) {
parentNoteId = allNoteIds[Math.floor(Math.random() * Math.min(allNoteIds.length, 100))];
}
// Create note
createNote({
noteId,
title: generateTitle(),
content: generateContent(),
type,
parentNoteId,
isProtected: Math.random() < 0.05
});
notesCreated++;
allNoteIds.push(noteId);
// Add attributes
const attributeCount = Math.floor(Math.random() * 5);
for (let a = 0; a < attributeCount; a++) {
const attrType = Math.random() < 0.7 ? 'label' : 'relation';
const attrName = attributeNames[Math.floor(Math.random() * attributeNames.length)];
// Decide parent - either container or random existing note
let parentNoteId = containerNoteId;
if (allNoteIds.length > 10 && Math.random() < 0.3) {
parentNoteId = allNoteIds[Math.floor(Math.random() * Math.min(allNoteIds.length, 100))];
}
// Create note
createNote(db!, {
noteId,
title: generateTitle(),
content: generateContent(),
type,
parentNoteId,
isProtected: Math.random() < 0.05
});
notesCreated++;
allNoteIds.push(noteId);
// Add attributes
const attributeCount = Math.floor(Math.random() * 5);
for (let a = 0; a < attributeCount; a++) {
const attrType = Math.random() < 0.7 ? 'label' : 'relation';
const attrName = attributeNames[Math.floor(Math.random() * attributeNames.length)];
try {
createAttribute(db!, {
noteId,
type: attrType as 'label' | 'relation',
name: attrName,
value: attrType === 'relation'
? allNoteIds[Math.floor(Math.random() * Math.min(allNoteIds.length, 50))]
: getRandomWord(),
isInheritable: Math.random() < 0.2
});
attributesCreated++;
} catch (e) {
// Ignore duplicate errors, but log unexpected ones
if (!(e instanceof Error) || !e.message.includes('UNIQUE')) {
console.warn(`Unexpected attribute error: ${e}`);
}
}
}
// Keep memory in check
if (allNoteIds.length > 500) {
allNoteIds.splice(1, allNoteIds.length - 500);
try {
createAttribute({
noteId,
type: attrType,
name: attrName,
value: attrType === 'relation'
? allNoteIds[Math.floor(Math.random() * Math.min(allNoteIds.length, 50))]
: getRandomWord(),
isInheritable: Math.random() < 0.2
});
attributesCreated++;
} catch (e) {
// Ignore duplicate errors
}
}
});
try {
batchTransaction();
const progress = Math.round(((batch + 1) / Math.ceil(noteCount / batchSize)) * 100);
const elapsed = (Date.now() - startTime) / 1000;
const rate = Math.round(notesCreated / elapsed);
console.log(`Progress: ${progress}% | Notes: ${notesCreated}/${noteCount} | Rate: ${rate}/sec | Attributes: ${attributesCreated}`);
} catch (error) {
console.error(`Failed to process batch ${batch + 1}:`, error);
throw error;
}
}
// Add entity changes
console.log('\nAdding entity changes...');
const entityTransaction = db.transaction(() => {
const stmt = db.prepare(`
INSERT OR REPLACE INTO entity_changes
(entityName, entityId, hash, isErased, changeId, componentId, instanceId, isSynced, utcDateChanged)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (let i = 0; i < Math.min(100, allNoteIds.length); i++) {
stmt.run(
'notes',
allNoteIds[i],
randomBytes(16).toString('hex'),
0,
newEntityId(),
'stress_test',
'stress_test_instance',
1,
utcNowDateTime()
);
// Keep memory in check
if (allNoteIds.length > 500) {
allNoteIds.splice(1, allNoteIds.length - 500);
}
}
});
try {
entityTransaction();
} catch (error) {
console.error('Failed to add entity changes:', error);
// Non-critical error, continue
}
batchTransaction();
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
const progress = Math.round(((batch + 1) / Math.ceil(noteCount / batchSize)) * 100);
const elapsed = (Date.now() - startTime) / 1000;
const rate = Math.round(notesCreated / elapsed);
// Get statistics
console.log('\nGathering database statistics...');
const stats = {
notes: db.prepare('SELECT COUNT(*) as count FROM notes').get() as any,
branches: db.prepare('SELECT COUNT(*) as count FROM branches').get() as any,
attributes: db.prepare('SELECT COUNT(*) as count FROM attributes').get() as any,
blobs: db.prepare('SELECT COUNT(*) as count FROM blobs').get() as any
};
console.log('\n✅ Native-style stress test completed successfully!\n');
console.log('Database Statistics:');
console.log(` • Total notes: ${stats.notes.count.toLocaleString()}`);
console.log(` • Total branches: ${stats.branches.count.toLocaleString()}`);
console.log(` • Total attributes: ${stats.attributes.count.toLocaleString()}`);
console.log(` • Total blobs: ${stats.blobs.count.toLocaleString()}`);
console.log(` • Time taken: ${duration.toFixed(2)} seconds`);
console.log(` • Average rate: ${Math.round(noteCount / duration).toLocaleString()} notes/second`);
console.log(` • Container note ID: ${containerNoteId}\n`);
} catch (error) {
console.error('\n❌ Stress test failed with error:', error);
if (error instanceof Error) {
console.error('Error stack:', error.stack);
}
exitCode = 1;
} finally {
// Ensure cleanup happens
console.log('\nPerforming final cleanup...');
await resourceManager.cleanup();
// Exit with appropriate code
console.log(`Exiting with code: ${exitCode}`);
process.exit(exitCode);
console.log(`Progress: ${progress}% | Notes: ${notesCreated}/${noteCount} | Rate: ${rate}/sec | Attributes: ${attributesCreated}`);
}
// Add entity changes
console.log('\nAdding entity changes...');
const entityTransaction = db.transaction(() => {
const stmt = db.prepare(`
INSERT OR REPLACE INTO entity_changes
(entityName, entityId, hash, isErased, changeId, componentId, instanceId, isSynced, utcDateChanged)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (let i = 0; i < Math.min(100, allNoteIds.length); i++) {
stmt.run(
'notes',
allNoteIds[i],
randomBytes(16).toString('hex'),
0,
newEntityId(),
'stress_test',
'stress_test_instance',
1,
utcNowDateTime()
);
}
});
entityTransaction();
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
// Get statistics
const stats = {
notes: db.prepare('SELECT COUNT(*) as count FROM notes').get() as any,
branches: db.prepare('SELECT COUNT(*) as count FROM branches').get() as any,
attributes: db.prepare('SELECT COUNT(*) as count FROM attributes').get() as any,
blobs: db.prepare('SELECT COUNT(*) as count FROM blobs').get() as any
};
console.log('\n✅ Native-style stress test completed successfully!\n');
console.log('Database Statistics:');
console.log(` • Total notes: ${stats.notes.count.toLocaleString()}`);
console.log(` • Total branches: ${stats.branches.count.toLocaleString()}`);
console.log(` • Total attributes: ${stats.attributes.count.toLocaleString()}`);
console.log(` • Total blobs: ${stats.blobs.count.toLocaleString()}`);
console.log(` • Time taken: ${duration.toFixed(2)} seconds`);
console.log(` • Average rate: ${Math.round(noteCount / duration).toLocaleString()} notes/second`);
console.log(` • Container note ID: ${containerNoteId}\n`);
db.close();
}
// Run the main function
main().catch(async (error) => {
console.error('Fatal error in main:', error);
await resourceManager.cleanup();
main().catch((error) => {
console.error('Error:', error);
process.exit(1);
});

View File

@@ -15,75 +15,6 @@
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
process.env.DATA_DIR = process.env.DATA_DIR || './data';
// Resource manager for proper cleanup
class ResourceManager {
private resources: Array<{ name: string; cleanup: () => void | Promise<void> }> = [];
private cleanedUp = false;
register(name: string, cleanup: () => void | Promise<void>): void {
console.log(`[ResourceManager] Registered resource: ${name}`);
this.resources.push({ name, cleanup });
}
async cleanup(): Promise<void> {
if (this.cleanedUp) {
console.log('[ResourceManager] Already cleaned up, skipping...');
return;
}
console.log('[ResourceManager] Starting cleanup...');
this.cleanedUp = true;
// Cleanup in reverse order of registration
for (let i = this.resources.length - 1; i >= 0; i--) {
const resource = this.resources[i];
try {
console.log(`[ResourceManager] Cleaning up: ${resource.name}`);
await resource.cleanup();
console.log(`[ResourceManager] Successfully cleaned up: ${resource.name}`);
} catch (error) {
console.error(`[ResourceManager] Error cleaning up ${resource.name}:`, error);
}
}
this.resources = [];
console.log('[ResourceManager] Cleanup completed');
}
}
// Global resource manager
const resourceManager = new ResourceManager();
// Setup process exit handlers
process.on('exit', (code) => {
console.log(`[Process] Exiting with code: ${code}`);
});
process.on('SIGINT', async () => {
console.log('\n[Process] Received SIGINT, cleaning up...');
await resourceManager.cleanup();
process.exit(130); // Standard exit code for SIGINT
});
process.on('SIGTERM', async () => {
console.log('\n[Process] Received SIGTERM, cleaning up...');
await resourceManager.cleanup();
process.exit(143); // Standard exit code for SIGTERM
});
process.on('uncaughtException', async (error) => {
console.error('[Process] Uncaught exception:', error);
await resourceManager.cleanup();
process.exit(1);
});
process.on('unhandledRejection', async (reason, promise) => {
console.error('[Process] Unhandled rejection at:', promise, 'reason:', reason);
await resourceManager.cleanup();
process.exit(1);
});
// Import Trilium services after setting up environment and handlers
import './src/becca/entity_constructor.js';
import sqlInit from './src/services/sql_init.js';
import noteService from './src/services/notes.js';
@@ -95,7 +26,6 @@ import becca from './src/becca/becca.js';
import entityChangesService from './src/services/entity_changes.js';
import type BNote from './src/becca/entities/bnote.js';
// Parse command line arguments
const noteCount = parseInt(process.argv[2]);
const batchSize = parseInt(process.argv[3]) || 100;
@@ -229,8 +159,7 @@ function generateSentence(): string {
return wordList.join(' ');
}
async function runStressTest(): Promise<void> {
let exitCode = 0;
async function start() {
const startTime = Date.now();
const allNotes: BNote[] = [];
let notesCreated = 0;
@@ -238,343 +167,255 @@ async function runStressTest(): Promise<void> {
let clonesCreated = 0;
let revisionsCreated = 0;
try {
console.log('Starting note generation using native Trilium services...\n');
// Find root note
const rootNote = becca.getNote('root');
if (!rootNote) {
throw new Error('Root note not found! Database might not be initialized properly.');
}
// Create a container note for our stress test
console.log('Creating container note...');
const { note: containerNote } = noteService.createNewNote({
parentNoteId: 'root',
title: `Stress Test ${new Date().toISOString()}`,
content: `<p>Container for stress test with ${noteCount} notes</p>`,
type: 'text',
isProtected: false
});
console.log(`Created container note: ${containerNote.title} (${containerNote.noteId})`);
allNotes.push(containerNote);
// Process in batches for better control
for (let batch = 0; batch < Math.ceil(noteCount / batchSize); batch++) {
const batchStart = batch * batchSize;
const batchEnd = Math.min(batchStart + batchSize, noteCount);
const batchNoteCount = batchEnd - batchStart;
try {
sql.transactional(() => {
for (let i = 0; i < batchNoteCount; i++) {
const type = noteTypes[Math.floor(Math.random() * noteTypes.length)];
let content = '';
let mime = undefined;
// Generate content based on type
switch (type) {
case 'code':
content = generateCodeContent();
mime = 'text/plain';
break;
case 'mermaid':
content = generateMermaidContent();
mime = 'text/plain';
break;
case 'canvas':
content = JSON.stringify({
elements: [],
appState: { viewBackgroundColor: "#ffffff" },
files: {}
});
mime = 'application/json';
break;
case 'search':
content = JSON.stringify({
searchString: `#${getRandomWord()} OR #${getRandomWord()}`
});
mime = 'application/json';
break;
case 'relationMap':
content = JSON.stringify({
notes: [],
zoom: 1
});
mime = 'application/json';
break;
default:
content = generateContent();
mime = 'text/html';
}
// Decide parent - either container or random existing note for complex hierarchy
let parentNoteId = containerNote.noteId;
if (allNotes.length > 10 && Math.random() < 0.3) {
// 30% chance to attach to random existing note
parentNoteId = allNotes[Math.floor(Math.random() * Math.min(allNotes.length, 100))].noteId;
}
// Create the note using native service
const { note, branch } = noteService.createNewNote({
parentNoteId,
title: generateTitle(),
content,
type,
mime,
isProtected: Math.random() < 0.05 // 5% protected notes
});
notesCreated++;
allNotes.push(note);
// Add attributes using native service
const attributeCount = Math.floor(Math.random() * 8);
for (let a = 0; a < attributeCount; a++) {
const attrType = Math.random() < 0.7 ? 'label' : 'relation';
const attrName = attributeNames[Math.floor(Math.random() * attributeNames.length)];
try {
if (attrType === 'label') {
attributeService.createLabel(
note.noteId,
attrName,
Math.random() < 0.5 ? getRandomWord() : ''
);
attributesCreated++;
} else if (allNotes.length > 1) {
const targetNote = allNotes[Math.floor(Math.random() * Math.min(allNotes.length, 50))];
attributeService.createRelation(
note.noteId,
attrName,
targetNote.noteId
);
attributesCreated++;
}
} catch (e) {
// Ignore attribute creation errors (e.g., duplicates)
if (e instanceof Error && !e.message.includes('duplicate') && !e.message.includes('already exists')) {
console.warn(`Unexpected attribute error: ${e.message}`);
}
}
}
// Update note content occasionally to trigger revisions
if (Math.random() < 0.1) { // 10% chance
note.setContent(content + `\n<p>Updated at ${new Date().toISOString()}</p>`);
note.save();
// Save revision
if (Math.random() < 0.5) {
try {
note.saveRevision();
revisionsCreated++;
} catch (e) {
// Ignore revision errors
}
}
}
// Create clones occasionally for complex relationships
if (allNotes.length > 20 && Math.random() < 0.05) { // 5% chance
try {
const targetParent = allNotes[Math.floor(Math.random() * allNotes.length)];
const result = cloningService.cloneNoteToBranch(
note.noteId,
targetParent.noteId,
Math.random() < 0.2 ? 'clone' : ''
);
if (result.success) {
clonesCreated++;
}
} catch (e) {
// Ignore cloning errors (e.g., circular dependencies)
}
}
// Add note to recent notes occasionally
if (Math.random() < 0.1) { // 10% chance
try {
sql.execute(
"INSERT OR IGNORE INTO recent_notes (noteId, notePath, utcDateCreated) VALUES (?, ?, ?)",
[note.noteId, note.getBestNotePath()?.path || 'root', note.utcDateCreated]
);
} catch (e) {
// Table might not exist in all versions
}
}
// Keep memory usage in check
if (allNotes.length > 500) {
allNotes.splice(0, allNotes.length - 500);
}
}
})();
const progress = Math.round(((batch + 1) / Math.ceil(noteCount / batchSize)) * 100);
const elapsed = (Date.now() - startTime) / 1000;
const rate = Math.round(notesCreated / elapsed);
console.log(`Progress: ${progress}% | Notes: ${notesCreated}/${noteCount} | Rate: ${rate}/sec | Attrs: ${attributesCreated} | Clones: ${clonesCreated} | Revisions: ${revisionsCreated}`);
} catch (error) {
console.error(`Failed to process batch ${batch + 1}:`, error);
throw error;
}
// Force entity changes sync (non-critical)
try {
entityChangesService.putNoteReorderingEntityChange(containerNote.noteId);
} catch (e) {
// Ignore entity change errors
}
}
// Create some advanced structures
console.log('\nCreating advanced relationships...');
try {
// Create template notes
const templateNote = noteService.createNewNote({
parentNoteId: containerNote.noteId,
title: 'Template: ' + generateTitle(),
content: '<p>This is a template note</p>',
type: 'text',
isProtected: false
}).note;
attributeService.createLabel(templateNote.noteId, 'template', '');
// Apply template to some notes
for (let i = 0; i < Math.min(10, allNotes.length); i++) {
const targetNote = allNotes[Math.floor(Math.random() * allNotes.length)];
try {
attributeService.createRelation(targetNote.noteId, 'template', templateNote.noteId);
} catch (e) {
// Ignore relation errors
}
}
// Create some CSS notes
const cssNote = noteService.createNewNote({
parentNoteId: containerNote.noteId,
title: 'Custom CSS',
content: `.custom-class { color: #${Math.floor(Math.random()*16777215).toString(16)}; }`,
type: 'code',
mime: 'text/css',
isProtected: false
}).note;
attributeService.createLabel(cssNote.noteId, 'appCss', '');
// Create widget notes
const widgetNote = noteService.createNewNote({
parentNoteId: containerNote.noteId,
title: 'Custom Widget',
content: `<div>Widget content: ${generateSentence()}</div>`,
type: 'code',
mime: 'text/html',
isProtected: false
}).note;
attributeService.createLabel(widgetNote.noteId, 'widget', '');
} catch (error) {
console.warn('Failed to create some advanced structures:', error);
// Non-critical, continue
}
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
// Get final statistics
console.log('\nGathering database statistics...');
let stats: any = {};
try {
stats.notes = sql.getValue('SELECT COUNT(*) FROM notes');
stats.branches = sql.getValue('SELECT COUNT(*) FROM branches');
stats.attributes = sql.getValue('SELECT COUNT(*) FROM attributes');
stats.revisions = sql.getValue('SELECT COUNT(*) FROM revisions');
stats.attachments = sql.getValue('SELECT COUNT(*) FROM attachments');
stats.recentNotes = sql.getValue('SELECT COUNT(*) FROM recent_notes');
} catch (error) {
console.warn('Failed to get some statistics:', error);
}
console.log('\n✅ Native API stress test completed successfully!\n');
console.log('Database Statistics:');
console.log(` • Total notes: ${stats.notes?.toLocaleString() || 'N/A'}`);
console.log(` • Total branches: ${stats.branches?.toLocaleString() || 'N/A'}`);
console.log(` • Total attributes: ${stats.attributes?.toLocaleString() || 'N/A'}`);
console.log(` • Total revisions: ${stats.revisions?.toLocaleString() || 'N/A'}`);
console.log(` • Total attachments: ${stats.attachments?.toLocaleString() || 'N/A'}`);
console.log(` • Recent notes: ${stats.recentNotes?.toLocaleString() || 'N/A'}`);
console.log(` • Time taken: ${duration.toFixed(2)} seconds`);
console.log(` • Average rate: ${Math.round(noteCount / duration).toLocaleString()} notes/second`);
console.log(` • Container note ID: ${containerNote.noteId}\n`);
} catch (error) {
console.error('\n❌ Stress test failed with error:', error);
if (error instanceof Error) {
console.error('Error stack:', error.stack);
}
exitCode = 1;
} finally {
// Cleanup database connections and resources
console.log('\nCleaning up database resources...');
try {
// Close any open database connections
if (sql && typeof sql.execute === 'function') {
// Try to checkpoint WAL if possible
try {
sql.execute('PRAGMA wal_checkpoint(TRUNCATE)');
console.log('WAL checkpoint completed');
} catch (e) {
// Ignore checkpoint errors
}
}
} catch (error) {
console.warn('Error during database cleanup:', error);
}
// Perform final resource cleanup
await resourceManager.cleanup();
// Exit with appropriate code
console.log(`Exiting with code: ${exitCode}`);
process.exit(exitCode);
}
}
async function start(): Promise<void> {
try {
// Register database cleanup
resourceManager.register('Database Connection', async () => {
try {
if (sql && typeof sql.execute === 'function') {
console.log('Closing database connections...');
// Attempt to close any open transactions
sql.execute('ROLLBACK');
}
} catch (e) {
// Ignore errors during cleanup
}
});
// Run the stress test
await runStressTest();
} catch (error) {
console.error('Fatal error during startup:', error);
await resourceManager.cleanup();
console.log('Starting note generation using native Trilium services...\n');
// Find root note
const rootNote = becca.getNote('root');
if (!rootNote) {
console.error('Root note not found!');
process.exit(1);
}
// Create a container note for our stress test
const { note: containerNote } = noteService.createNewNote({
parentNoteId: 'root',
title: `Stress Test ${new Date().toISOString()}`,
content: `<p>Container for stress test with ${noteCount} notes</p>`,
type: 'text',
isProtected: false
});
console.log(`Created container note: ${containerNote.title} (${containerNote.noteId})`);
allNotes.push(containerNote);
// Process in batches for better control
for (let batch = 0; batch < Math.ceil(noteCount / batchSize); batch++) {
const batchStart = batch * batchSize;
const batchEnd = Math.min(batchStart + batchSize, noteCount);
const batchNoteCount = batchEnd - batchStart;
sql.transactional(() => {
for (let i = 0; i < batchNoteCount; i++) {
const type = noteTypes[Math.floor(Math.random() * noteTypes.length)];
let content = '';
let mime = undefined;
// Generate content based on type
switch (type) {
case 'code':
content = generateCodeContent();
mime = 'text/plain';
break;
case 'mermaid':
content = generateMermaidContent();
mime = 'text/plain';
break;
case 'canvas':
content = JSON.stringify({
elements: [],
appState: { viewBackgroundColor: "#ffffff" },
files: {}
});
mime = 'application/json';
break;
case 'search':
content = JSON.stringify({
searchString: `#${getRandomWord()} OR #${getRandomWord()}`
});
mime = 'application/json';
break;
case 'relationMap':
content = JSON.stringify({
notes: [],
zoom: 1
});
mime = 'application/json';
break;
default:
content = generateContent();
mime = 'text/html';
}
// Decide parent - either container or random existing note for complex hierarchy
let parentNoteId = containerNote.noteId;
if (allNotes.length > 10 && Math.random() < 0.3) {
// 30% chance to attach to random existing note
parentNoteId = allNotes[Math.floor(Math.random() * Math.min(allNotes.length, 100))].noteId;
}
// Create the note using native service
const { note, branch } = noteService.createNewNote({
parentNoteId,
title: generateTitle(),
content,
type,
mime,
isProtected: Math.random() < 0.05 // 5% protected notes
});
notesCreated++;
allNotes.push(note);
// Add attributes using native service
const attributeCount = Math.floor(Math.random() * 8);
for (let a = 0; a < attributeCount; a++) {
const attrType = Math.random() < 0.7 ? 'label' : 'relation';
const attrName = attributeNames[Math.floor(Math.random() * attributeNames.length)];
try {
if (attrType === 'label') {
attributeService.createLabel(
note.noteId,
attrName,
Math.random() < 0.5 ? getRandomWord() : ''
);
attributesCreated++;
} else if (allNotes.length > 1) {
const targetNote = allNotes[Math.floor(Math.random() * Math.min(allNotes.length, 50))];
attributeService.createRelation(
note.noteId,
attrName,
targetNote.noteId
);
attributesCreated++;
}
} catch (e) {
// Ignore attribute creation errors (e.g., duplicates)
}
}
// Update note content occasionally to trigger revisions
if (Math.random() < 0.1) { // 10% chance
note.setContent(content + `\n<p>Updated at ${new Date().toISOString()}</p>`);
note.save();
// Save revision
if (Math.random() < 0.5) {
note.saveRevision();
revisionsCreated++;
}
}
// Create clones occasionally for complex relationships
if (allNotes.length > 20 && Math.random() < 0.05) { // 5% chance
try {
const targetParent = allNotes[Math.floor(Math.random() * allNotes.length)];
const result = cloningService.cloneNoteToBranch(
note.noteId,
targetParent.noteId,
Math.random() < 0.2 ? 'clone' : ''
);
if (result.success) {
clonesCreated++;
}
} catch (e) {
// Ignore cloning errors (e.g., circular dependencies)
}
}
// Add note to recent notes occasionally
if (Math.random() < 0.1) { // 10% chance
try {
sql.execute(
"INSERT OR IGNORE INTO recent_notes (noteId, notePath, utcDateCreated) VALUES (?, ?, ?)",
[note.noteId, note.getBestNotePath()?.path || 'root', note.utcDateCreated]
);
} catch (e) {
// Table might not exist in all versions
}
}
// Keep memory usage in check
if (allNotes.length > 500) {
allNotes.splice(0, allNotes.length - 500);
}
}
})();
const progress = Math.round(((batch + 1) / Math.ceil(noteCount / batchSize)) * 100);
const elapsed = (Date.now() - startTime) / 1000;
const rate = Math.round(notesCreated / elapsed);
console.log(`Progress: ${progress}% | Notes: ${notesCreated}/${noteCount} | Rate: ${rate}/sec | Attrs: ${attributesCreated} | Clones: ${clonesCreated} | Revisions: ${revisionsCreated}`);
// Force entity changes sync
entityChangesService.putNoteReorderingEntityChange(containerNote.noteId);
}
// Create some advanced structures
console.log('\nCreating advanced relationships...');
// Create template notes
const templateNote = noteService.createNewNote({
parentNoteId: containerNote.noteId,
title: 'Template: ' + generateTitle(),
content: '<p>This is a template note</p>',
type: 'text',
isProtected: false
}).note;
attributeService.createLabel(templateNote.noteId, 'template', '');
// Apply template to some notes
for (let i = 0; i < Math.min(10, allNotes.length); i++) {
const targetNote = allNotes[Math.floor(Math.random() * allNotes.length)];
attributeService.createRelation(targetNote.noteId, 'template', templateNote.noteId);
}
// Create some CSS notes
const cssNote = noteService.createNewNote({
parentNoteId: containerNote.noteId,
title: 'Custom CSS',
content: `.custom-class { color: #${Math.floor(Math.random()*16777215).toString(16)}; }`,
type: 'code',
mime: 'text/css',
isProtected: false
}).note;
attributeService.createLabel(cssNote.noteId, 'appCss', '');
// Create widget notes
const widgetNote = noteService.createNewNote({
parentNoteId: containerNote.noteId,
title: 'Custom Widget',
content: `<div>Widget content: ${generateSentence()}</div>`,
type: 'code',
mime: 'text/html',
isProtected: false
}).note;
attributeService.createLabel(widgetNote.noteId, 'widget', '');
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
// Get final statistics
const stats = {
notes: sql.getValue('SELECT COUNT(*) FROM notes'),
branches: sql.getValue('SELECT COUNT(*) FROM branches'),
attributes: sql.getValue('SELECT COUNT(*) FROM attributes'),
revisions: sql.getValue('SELECT COUNT(*) FROM revisions'),
attachments: sql.getValue('SELECT COUNT(*) FROM attachments'),
recentNotes: sql.getValue('SELECT COUNT(*) FROM recent_notes')
};
console.log('\n✅ Native API stress test completed successfully!\n');
console.log('Database Statistics:');
console.log(` • Total notes: ${stats.notes?.toLocaleString()}`);
console.log(` • Total branches: ${stats.branches?.toLocaleString()}`);
console.log(` • Total attributes: ${stats.attributes?.toLocaleString()}`);
console.log(` • Total revisions: ${stats.revisions?.toLocaleString()}`);
console.log(` • Total attachments: ${stats.attachments?.toLocaleString()}`);
console.log(` • Recent notes: ${stats.recentNotes?.toLocaleString()}`);
console.log(` • Time taken: ${duration.toFixed(2)} seconds`);
console.log(` • Average rate: ${Math.round(noteCount / duration).toLocaleString()} notes/second`);
console.log(` • Container note ID: ${containerNote.noteId}\n`);
process.exit(0);
}
// Initialize database and run stress test
sqlInit.dbReady
.then(() => cls.wrap(start)())
.catch(async (err) => {
console.error('Failed to initialize database:', err);
await resourceManager.cleanup();
process.exit(1);
});
sqlInit.dbReady.then(cls.wrap(start)).catch((err) => {
console.error('Error:', err);
process.exit(1);
});