mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
test: COD-30 stabilize perf browser timing
This commit is contained in:
+130
-98
@@ -19,24 +19,24 @@ const BASE_URL = `http://localhost:${PORT}`;
|
|||||||
|
|
||||||
// Thresholds (ms)
|
// Thresholds (ms)
|
||||||
const THRESHOLDS = {
|
const THRESHOLDS = {
|
||||||
PAGE_LOAD: 3000, // Full page load including JS init
|
PAGE_LOAD: 3000, // Full page load including JS init
|
||||||
DOMContentLoaded: 1500, // HTML parsed
|
DOMContentLoaded: 1500, // Browser nav timing through deferred script execution
|
||||||
SSE_CONNECT: 2000, // SSE EventSource open
|
SSE_CONNECT: 2000, // SSE EventSource open
|
||||||
TAB_CREATE_API: 200, // POST /api/sessions response
|
TAB_CREATE_API: 200, // POST /api/sessions response
|
||||||
TAB_RENDER: 300, // Tab element appears in DOM
|
TAB_RENDER: 300, // Tab element appears in DOM
|
||||||
TAB_SWITCH: 400, // Tab click to active class applied (includes tmux session creation)
|
TAB_SWITCH: 400, // Tab click to active class applied (includes tmux session creation)
|
||||||
TERMINAL_INIT: 500, // xterm.js instance created for tab
|
TERMINAL_INIT: 500, // xterm.js instance created for tab
|
||||||
INPUT_ROUNDTRIP: 500, // Keystroke sent via API → acknowledged
|
INPUT_ROUNDTRIP: 500, // Keystroke sent via API → acknowledged
|
||||||
SETTINGS_OPEN: 300, // Settings modal visible
|
SETTINGS_OPEN: 300, // Settings modal visible
|
||||||
SETTINGS_CLOSE: 200, // Settings modal hidden
|
SETTINGS_CLOSE: 200, // Settings modal hidden
|
||||||
SESSION_OPTIONS_OPEN: 300, // Session options modal visible
|
SESSION_OPTIONS_OPEN: 300, // Session options modal visible
|
||||||
SESSION_OPTIONS_TAB: 200, // Modal tab switch
|
SESSION_OPTIONS_TAB: 200, // Modal tab switch
|
||||||
SUBAGENT_WINDOW_OPEN: 400, // Subagent window rendered
|
SUBAGENT_WINDOW_OPEN: 400, // Subagent window rendered
|
||||||
SUBAGENT_WINDOW_CLOSE: 200,
|
SUBAGENT_WINDOW_CLOSE: 200,
|
||||||
BULK_TAB_CREATE: 3000, // Create 10 sessions
|
BULK_TAB_CREATE: 3000, // Create 10 sessions
|
||||||
BULK_TAB_SWITCH_AVG: 300, // Average per-tab switch across 10 tabs (includes buffer loads)
|
BULK_TAB_SWITCH_AVG: 300, // Average per-tab switch across 10 tabs (includes buffer loads)
|
||||||
MEMORY_HEAP_MB: 200, // Max JS heap after heavy load
|
MEMORY_HEAP_MB: 200, // Max JS heap after heavy load
|
||||||
BUFFER_LOAD_16KB: 500, // Load a 16KB terminal buffer
|
BUFFER_LOAD_16KB: 500, // Load a 16KB terminal buffer
|
||||||
};
|
};
|
||||||
|
|
||||||
let server: WebServer;
|
let server: WebServer;
|
||||||
@@ -88,6 +88,25 @@ async function measure(fn: () => Promise<void>): Promise<number> {
|
|||||||
return performance.now() - start;
|
return performance.now() - start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BrowserNavigationTiming = {
|
||||||
|
domInteractive: number;
|
||||||
|
domContentLoadedEventEnd: number;
|
||||||
|
loadEventEnd: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Get the browser's own navigation timing, excluding Playwright harness overhead. */
|
||||||
|
async function getBrowserNavigationTiming(page: Page): Promise<BrowserNavigationTiming> {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const entry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming | undefined;
|
||||||
|
if (!entry) throw new Error('Navigation timing entry not available');
|
||||||
|
return {
|
||||||
|
domInteractive: entry.domInteractive,
|
||||||
|
domContentLoadedEventEnd: entry.domContentLoadedEventEnd,
|
||||||
|
loadEventEnd: entry.loadEventEnd,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** Get JS heap size in MB (Chromium only) */
|
/** Get JS heap size in MB (Chromium only) */
|
||||||
async function getHeapMB(page: Page): Promise<number> {
|
async function getHeapMB(page: Page): Promise<number> {
|
||||||
const metrics = await page.evaluate(() => {
|
const metrics = await page.evaluate(() => {
|
||||||
@@ -123,12 +142,15 @@ describe('Page load performance', () => {
|
|||||||
it('DOMContentLoaded fires within threshold', async () => {
|
it('DOMContentLoaded fires within threshold', async () => {
|
||||||
({ context, page } = await freshPage());
|
({ context, page } = await freshPage());
|
||||||
|
|
||||||
const timing = await measure(async () => {
|
const wallTiming = await measure(async () => {
|
||||||
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
|
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
|
||||||
});
|
});
|
||||||
|
const navigationTiming = await getBrowserNavigationTiming(page);
|
||||||
|
|
||||||
console.log(`[page load] DOMContentLoaded: ${timing.toFixed(0)}ms`);
|
console.log(
|
||||||
expect(timing).toBeLessThan(THRESHOLDS.DOMContentLoaded);
|
`[page load] DOMContentLoaded: ${navigationTiming.domContentLoadedEventEnd.toFixed(0)}ms (wall ${wallTiming.toFixed(0)}ms)`
|
||||||
|
);
|
||||||
|
expect(navigationTiming.domContentLoadedEventEnd).toBeLessThan(THRESHOLDS.DOMContentLoaded);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('full app initialization completes within threshold', async () => {
|
it('full app initialization completes within threshold', async () => {
|
||||||
@@ -157,7 +179,7 @@ describe('Page load performance', () => {
|
|||||||
const dot = document.getElementById('connectionDot');
|
const dot = document.getElementById('connectionDot');
|
||||||
return dot?.classList.contains('connected') || indicator.style.display === 'none';
|
return dot?.classList.contains('connected') || indicator.style.display === 'none';
|
||||||
},
|
},
|
||||||
{ timeout: 5000 },
|
{ timeout: 5000 }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -225,7 +247,7 @@ describe('Session tab creation', () => {
|
|||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
(expected: number) => document.querySelectorAll('.session-tab').length > expected,
|
(expected: number) => document.querySelectorAll('.session-tab').length > expected,
|
||||||
tabCountBefore,
|
tabCountBefore,
|
||||||
{ timeout: 3000 },
|
{ timeout: 3000 }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -250,7 +272,7 @@ describe('Session tab creation', () => {
|
|||||||
if (!container) return false;
|
if (!container) return false;
|
||||||
return container.querySelector('.xterm-screen') !== null;
|
return container.querySelector('.xterm-screen') !== null;
|
||||||
},
|
},
|
||||||
{ timeout: 3000 },
|
{ timeout: 3000 }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -277,7 +299,7 @@ describe('Tab switching performance', () => {
|
|||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
(count: number) => document.querySelectorAll('.session-tab').length >= count,
|
(count: number) => document.querySelectorAll('.session-tab').length >= count,
|
||||||
sessionIds.length,
|
sessionIds.length,
|
||||||
{ timeout: 5000 },
|
{ timeout: 5000 }
|
||||||
);
|
);
|
||||||
// Select first tab
|
// Select first tab
|
||||||
await page.locator(`.session-tab[data-id="${sessionIds[0]}"]`).click();
|
await page.locator(`.session-tab[data-id="${sessionIds[0]}"]`).click();
|
||||||
@@ -303,7 +325,7 @@ describe('Tab switching performance', () => {
|
|||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
(id: string) => document.querySelector(`.session-tab[data-id="${id}"]`)?.classList.contains('active'),
|
(id: string) => document.querySelector(`.session-tab[data-id="${id}"]`)?.classList.contains('active'),
|
||||||
targetId,
|
targetId,
|
||||||
{ timeout: 2000 },
|
{ timeout: 2000 }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
timings.push(timing);
|
timings.push(timing);
|
||||||
@@ -325,7 +347,7 @@ describe('Tab switching performance', () => {
|
|||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
(id: string) => document.querySelector(`.session-tab[data-id="${id}"]`)?.classList.contains('active'),
|
(id: string) => document.querySelector(`.session-tab[data-id="${id}"]`)?.classList.contains('active'),
|
||||||
targetId,
|
targetId,
|
||||||
{ timeout: 2000 },
|
{ timeout: 2000 }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
timings.push(timing);
|
timings.push(timing);
|
||||||
@@ -362,11 +384,9 @@ describe('Bulk tab operations', () => {
|
|||||||
sessionIds.push(id);
|
sessionIds.push(id);
|
||||||
}
|
}
|
||||||
// Wait for all tabs to render
|
// Wait for all tabs to render
|
||||||
await page.waitForFunction(
|
await page.waitForFunction((count: number) => document.querySelectorAll('.session-tab').length >= count, 10, {
|
||||||
(count: number) => document.querySelectorAll('.session-tab').length >= count,
|
timeout: 5000,
|
||||||
10,
|
});
|
||||||
{ timeout: 5000 },
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[bulk create] 10 sessions: ${timing.toFixed(0)}ms`);
|
console.log(`[bulk create] 10 sessions: ${timing.toFixed(0)}ms`);
|
||||||
@@ -383,7 +403,7 @@ describe('Bulk tab operations', () => {
|
|||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
(id: string) => document.querySelector(`.session-tab[data-id="${id}"]`)?.classList.contains('active'),
|
(id: string) => document.querySelector(`.session-tab[data-id="${id}"]`)?.classList.contains('active'),
|
||||||
targetId,
|
targetId,
|
||||||
{ timeout: 2000 },
|
{ timeout: 2000 }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
timings.push(timing);
|
timings.push(timing);
|
||||||
@@ -497,7 +517,10 @@ describe('Settings modal performance', () => {
|
|||||||
|
|
||||||
it('closes within threshold', async () => {
|
it('closes within threshold', async () => {
|
||||||
// Make sure it's open
|
// Make sure it's open
|
||||||
const isOpen = await page.locator('#appSettingsModal.active').isVisible().catch(() => false);
|
const isOpen = await page
|
||||||
|
.locator('#appSettingsModal.active')
|
||||||
|
.isVisible()
|
||||||
|
.catch(() => false);
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
await page.locator('.btn-settings').click();
|
await page.locator('.btn-settings').click();
|
||||||
await page.waitForSelector('#appSettingsModal.active', { timeout: 2000 });
|
await page.waitForSelector('#appSettingsModal.active', { timeout: 2000 });
|
||||||
@@ -506,10 +529,9 @@ describe('Settings modal performance', () => {
|
|||||||
const timing = await measure(async () => {
|
const timing = await measure(async () => {
|
||||||
// Press Escape to close
|
// Press Escape to close
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(() => !document.querySelector('#appSettingsModal')?.classList.contains('active'), {
|
||||||
() => !document.querySelector('#appSettingsModal')?.classList.contains('active'),
|
timeout: 2000,
|
||||||
{ timeout: 2000 },
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[settings] close: ${timing.toFixed(0)}ms`);
|
console.log(`[settings] close: ${timing.toFixed(0)}ms`);
|
||||||
@@ -528,10 +550,9 @@ describe('Settings modal performance', () => {
|
|||||||
// Close
|
// Close
|
||||||
const closeTime = await measure(async () => {
|
const closeTime = await measure(async () => {
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(() => !document.querySelector('#appSettingsModal')?.classList.contains('active'), {
|
||||||
() => !document.querySelector('#appSettingsModal')?.classList.contains('active'),
|
timeout: 2000,
|
||||||
{ timeout: 2000 },
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
timings.push(openTime + closeTime);
|
timings.push(openTime + closeTime);
|
||||||
}
|
}
|
||||||
@@ -575,7 +596,10 @@ describe('Session options modal performance', () => {
|
|||||||
|
|
||||||
it('tab switching within modal is instant', async () => {
|
it('tab switching within modal is instant', async () => {
|
||||||
// Ensure modal is open
|
// Ensure modal is open
|
||||||
const isOpen = await page.locator('#sessionOptionsModal.active').isVisible().catch(() => false);
|
const isOpen = await page
|
||||||
|
.locator('#sessionOptionsModal.active')
|
||||||
|
.isVisible()
|
||||||
|
.catch(() => false);
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
await page.locator(`.session-tab[data-id="${sessionId}"] .tab-gear`).click();
|
await page.locator(`.session-tab[data-id="${sessionId}"] .tab-gear`).click();
|
||||||
await page.waitForSelector('#sessionOptionsModal.active', { timeout: 2000 });
|
await page.waitForSelector('#sessionOptionsModal.active', { timeout: 2000 });
|
||||||
@@ -590,7 +614,7 @@ describe('Session options modal performance', () => {
|
|||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
(t: string) => document.querySelector(`[data-tab="${t}"]`)?.classList.contains('active'),
|
(t: string) => document.querySelector(`[data-tab="${t}"]`)?.classList.contains('active'),
|
||||||
tab,
|
tab,
|
||||||
{ timeout: 1000 },
|
{ timeout: 1000 }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
timings.push(timing);
|
timings.push(timing);
|
||||||
@@ -634,27 +658,32 @@ describe('Subagent window simulation', () => {
|
|||||||
}, sessionId);
|
}, sessionId);
|
||||||
|
|
||||||
const timing = await measure(async () => {
|
const timing = await measure(async () => {
|
||||||
await page.evaluate(({ agentId, cSessionId }: { agentId: string; cSessionId: string }) => {
|
await page.evaluate(
|
||||||
const app = (window as unknown as {
|
({ agentId, cSessionId }: { agentId: string; cSessionId: string }) => {
|
||||||
app: {
|
const app = (
|
||||||
subagents: Map<string, Record<string, unknown>>;
|
window as unknown as {
|
||||||
openSubagentWindow: (id: string) => void;
|
app: {
|
||||||
}
|
subagents: Map<string, Record<string, unknown>>;
|
||||||
}).app;
|
openSubagentWindow: (id: string) => void;
|
||||||
// Inject fake agent data
|
};
|
||||||
app.subagents.set(agentId, {
|
}
|
||||||
agentId,
|
).app;
|
||||||
sessionId: cSessionId,
|
// Inject fake agent data
|
||||||
status: 'active',
|
app.subagents.set(agentId, {
|
||||||
description: 'Performance test agent',
|
agentId,
|
||||||
startedAt: Date.now(),
|
sessionId: cSessionId,
|
||||||
lastActivityAt: Date.now(),
|
status: 'active',
|
||||||
toolCallCount: 0,
|
description: 'Performance test agent',
|
||||||
entryCount: 0,
|
startedAt: Date.now(),
|
||||||
fileSize: 0,
|
lastActivityAt: Date.now(),
|
||||||
});
|
toolCallCount: 0,
|
||||||
app.openSubagentWindow(agentId);
|
entryCount: 0,
|
||||||
}, { agentId: 'perf-agent-1', cSessionId: claudeSessionId });
|
fileSize: 0,
|
||||||
|
});
|
||||||
|
app.openSubagentWindow(agentId);
|
||||||
|
},
|
||||||
|
{ agentId: 'perf-agent-1', cSessionId: claudeSessionId }
|
||||||
|
);
|
||||||
await page.waitForSelector('.subagent-window', { timeout: 3000 });
|
await page.waitForSelector('.subagent-window', { timeout: 3000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -679,7 +708,7 @@ describe('Subagent window simulation', () => {
|
|||||||
const el = document.getElementById('subagent-window-perf-agent-1');
|
const el = document.getElementById('subagent-window-perf-agent-1');
|
||||||
return el && el.style.display === 'none';
|
return el && el.style.display === 'none';
|
||||||
},
|
},
|
||||||
{ timeout: 2000 },
|
{ timeout: 2000 }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -698,33 +727,35 @@ describe('Subagent window simulation', () => {
|
|||||||
|
|
||||||
const timing = await measure(async () => {
|
const timing = await measure(async () => {
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
await page.evaluate(({ idx, cSessionId }: { idx: number; cSessionId: string }) => {
|
await page.evaluate(
|
||||||
const agentId = `perf-multi-agent-${idx}`;
|
({ idx, cSessionId }: { idx: number; cSessionId: string }) => {
|
||||||
const app = (window as unknown as {
|
const agentId = `perf-multi-agent-${idx}`;
|
||||||
app: {
|
const app = (
|
||||||
subagents: Map<string, Record<string, unknown>>;
|
window as unknown as {
|
||||||
openSubagentWindow: (id: string) => void;
|
app: {
|
||||||
}
|
subagents: Map<string, Record<string, unknown>>;
|
||||||
}).app;
|
openSubagentWindow: (id: string) => void;
|
||||||
app.subagents.set(agentId, {
|
};
|
||||||
agentId,
|
}
|
||||||
sessionId: cSessionId,
|
).app;
|
||||||
status: 'active',
|
app.subagents.set(agentId, {
|
||||||
description: `Perf agent ${idx}`,
|
agentId,
|
||||||
startedAt: Date.now(),
|
sessionId: cSessionId,
|
||||||
lastActivityAt: Date.now(),
|
status: 'active',
|
||||||
toolCallCount: 0,
|
description: `Perf agent ${idx}`,
|
||||||
entryCount: 0,
|
startedAt: Date.now(),
|
||||||
fileSize: 0,
|
lastActivityAt: Date.now(),
|
||||||
});
|
toolCallCount: 0,
|
||||||
app.openSubagentWindow(agentId);
|
entryCount: 0,
|
||||||
}, { idx: i, cSessionId: claudeSessionId });
|
fileSize: 0,
|
||||||
|
});
|
||||||
|
app.openSubagentWindow(agentId);
|
||||||
|
},
|
||||||
|
{ idx: i, cSessionId: claudeSessionId }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Wait for all 5
|
// Wait for all 5
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(() => document.querySelectorAll('.subagent-window').length >= 5, { timeout: 5000 });
|
||||||
() => document.querySelectorAll('.subagent-window').length >= 5,
|
|
||||||
{ timeout: 5000 },
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const windowCount = await page.locator('.subagent-window').count();
|
const windowCount = await page.locator('.subagent-window').count();
|
||||||
@@ -772,7 +803,7 @@ describe('SSE event throughput', () => {
|
|||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
(count: number) => document.querySelectorAll('.session-tab').length >= count,
|
(count: number) => document.querySelectorAll('.session-tab').length >= count,
|
||||||
sessionIds.length,
|
sessionIds.length,
|
||||||
{ timeout: 5000 },
|
{ timeout: 5000 }
|
||||||
);
|
);
|
||||||
const elapsed = performance.now() - start;
|
const elapsed = performance.now() - start;
|
||||||
|
|
||||||
@@ -797,7 +828,7 @@ describe('SSE event throughput', () => {
|
|||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
(count: number) => document.querySelectorAll('.session-tab').length >= count,
|
(count: number) => document.querySelectorAll('.session-tab').length >= count,
|
||||||
sessionIds.length,
|
sessionIds.length,
|
||||||
{ timeout: 5000 },
|
{ timeout: 5000 }
|
||||||
);
|
);
|
||||||
|
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
@@ -813,7 +844,7 @@ describe('SSE event throughput', () => {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{ timeout: 5000 },
|
{ timeout: 5000 }
|
||||||
);
|
);
|
||||||
const elapsed = performance.now() - start;
|
const elapsed = performance.now() - start;
|
||||||
|
|
||||||
@@ -880,11 +911,9 @@ describe('Memory usage under load', () => {
|
|||||||
const id = await createSession(page, `perf-mem-${i}`);
|
const id = await createSession(page, `perf-mem-${i}`);
|
||||||
sessionIds.push(id);
|
sessionIds.push(id);
|
||||||
}
|
}
|
||||||
await page.waitForFunction(
|
await page.waitForFunction((count: number) => document.querySelectorAll('.session-tab').length >= count, 10, {
|
||||||
(count: number) => document.querySelectorAll('.session-tab').length >= count,
|
timeout: 5000,
|
||||||
10,
|
});
|
||||||
{ timeout: 5000 },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Switch through all tabs
|
// Switch through all tabs
|
||||||
for (const id of sessionIds) {
|
for (const id of sessionIds) {
|
||||||
@@ -895,10 +924,13 @@ describe('Memory usage under load', () => {
|
|||||||
const heapAfter = await getHeapMB(page);
|
const heapAfter = await getHeapMB(page);
|
||||||
const heapGrowth = heapAfter - heapBefore;
|
const heapGrowth = heapAfter - heapBefore;
|
||||||
|
|
||||||
console.log(`[memory] before: ${heapBefore.toFixed(1)}MB, after: ${heapAfter.toFixed(1)}MB, growth: ${heapGrowth.toFixed(1)}MB`);
|
console.log(
|
||||||
|
`[memory] before: ${heapBefore.toFixed(1)}MB, after: ${heapAfter.toFixed(1)}MB, growth: ${heapGrowth.toFixed(1)}MB`
|
||||||
|
);
|
||||||
|
|
||||||
// Heap should stay under absolute limit
|
// Heap should stay under absolute limit
|
||||||
if (heapAfter > 0) { // memory API may not be available
|
if (heapAfter > 0) {
|
||||||
|
// memory API may not be available
|
||||||
expect(heapAfter).toBeLessThan(THRESHOLDS.MEMORY_HEAP_MB);
|
expect(heapAfter).toBeLessThan(THRESHOLDS.MEMORY_HEAP_MB);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user