Skip to content

Commit

Permalink
fix: restore buffer based old file removal
Browse files Browse the repository at this point in the history
  • Loading branch information
aorumbayev committed Oct 31, 2024
1 parent 553d81a commit 3704e6a
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 16 deletions.
52 changes: 51 additions & 1 deletion src/debugging/writeAVMDebugTrace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as os from 'os'
import * as path from 'path'
import { DEBUG_TRACES_DIR } from '../constants'
import { registerDebugEventHandlers } from '../index'
import { generateDebugTraceFilename } from './writeAVMDebugTrace'
import { cleanupOldFiles, generateDebugTraceFilename } from './writeAVMDebugTrace'

describe('writeAVMDebugTrace tests', () => {
const localnet = algorandFixture()
Expand Down Expand Up @@ -89,3 +89,53 @@ describe('generateDebugTraceFilename', () => {
expect(filename).toBe(`${timestamp}_lr${(mockResponse as SimulateResponse).lastRound}_${expectedPattern}.trace.avm.json`)
})
})

describe('cleanupOldFiles', () => {
let tempDir: string

beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'debug-traces-'))
})

afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true })
})

test('removes oldest files when buffer size is exceeded', async () => {
// Create test files with different timestamps and sizes
const testFiles = [
{ name: 'old.json', content: 'a'.repeat(1024 * 1024), mtime: new Date('2023-01-01') },
{ name: 'newer.json', content: 'b'.repeat(1024 * 1024), mtime: new Date('2023-01-02') },
{ name: 'newest.json', content: 'c'.repeat(1024 * 1024), mtime: new Date('2023-01-03') },
]

// Create files with specific timestamps
for (const file of testFiles) {
const filePath = path.join(tempDir, file.name)
await fs.writeFile(filePath, file.content)
await fs.utimes(filePath, file.mtime, file.mtime)
}

// Set buffer size to 2MB (should remove oldest file)
await cleanupOldFiles(2, tempDir)

// Check remaining files
const remainingFiles = await fs.readdir(tempDir)
expect(remainingFiles).toHaveLength(2)
expect(remainingFiles).toContain('newer.json')
expect(remainingFiles).toContain('newest.json')
expect(remainingFiles).not.toContain('old.json')
})

test('does nothing when total size is within buffer limit', async () => {
const content = 'a'.repeat(512 * 1024) // 512KB
await fs.writeFile(path.join(tempDir, 'file1.json'), content)
await fs.writeFile(path.join(tempDir, 'file2.json'), content)

// Set buffer size to 2MB (files total 1MB, should not remove anything)
await cleanupOldFiles(2, tempDir)

const remainingFiles = await fs.readdir(tempDir)
expect(remainingFiles).toHaveLength(2)
})
})
51 changes: 37 additions & 14 deletions src/debugging/writeAVMDebugTrace.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,50 @@
import { AVMTracesEventData } from '@algorandfoundation/algokit-utils'
import { SimulateResponse } from 'algosdk/dist/types/client/v2/algod/models/types'
import { DEBUG_TRACES_DIR } from '../constants'
import { getProjectRoot, joinPaths, writeToFile } from '../utils'
import { createDirForFilePathIfNotExists, formatTimestampUTC, getProjectRoot, joinPaths, writeToFile } from '../utils'

type TxnTypeCount = {
type: string
count: number
}

/**
* Formats a date to YYYYMMDD_HHMMSS in UTC, equivalent to algokit-utils-py format:
* datetime.now(tz=timezone.utc).strftime("%Y%m%d_%H%M%S")
* Removes old trace files when total size exceeds buffer limit
*/
export function formatTimestampUTC(date: Date): string {
// Get UTC components
const year = date.getUTCFullYear()
const month = String(date.getUTCMonth() + 1).padStart(2, '0') // Months are zero-based
const day = String(date.getUTCDate()).padStart(2, '0')
const hours = String(date.getUTCHours()).padStart(2, '0')
const minutes = String(date.getUTCMinutes()).padStart(2, '0')
const seconds = String(date.getUTCSeconds()).padStart(2, '0')
export async function cleanupOldFiles(bufferSizeMb: number, outputRootDir: string): Promise<void> {
const fs = await import('fs')
const path = await import('path')

// Format the datetime string
return `${year}${month}${day}_${hours}${minutes}${seconds}`
let totalSize = (
await Promise.all(
(await fs.promises.readdir(outputRootDir)).map(async (file) => (await fs.promises.stat(path.join(outputRootDir, file))).size),
)
).reduce((a, b) => a + b, 0)

if (totalSize > bufferSizeMb * 1024 * 1024) {
const files = await fs.promises.readdir(outputRootDir)
const fileStats = await Promise.all(
files.map(async (file) => {
const stats = await fs.promises.stat(path.join(outputRootDir, file))
return { file, mtime: stats.mtime, size: stats.size }
}),
)

// Sort by modification time (oldest first)
fileStats.sort((a, b) => a.mtime.getTime() - b.mtime.getTime())

// Remove oldest files until we're under the buffer size
while (totalSize > bufferSizeMb * 1024 * 1024 && fileStats.length > 0) {
const oldestFile = fileStats.shift()!
totalSize -= oldestFile.size
await fs.promises.unlink(path.join(outputRootDir, oldestFile.file))
}
}
}

/**
* Generates a descriptive filename for a debug trace based on transaction types
*/
export function generateDebugTraceFilename(simulateResponse: SimulateResponse, timestamp: string): string {
const txnGroups = simulateResponse.txnGroups
const txnTypesCount = txnGroups.reduce((acc: Map<string, TxnTypeCount>, txnGroup) => {
Expand Down Expand Up @@ -62,7 +82,7 @@ export function generateDebugTraceFilename(simulateResponse: SimulateResponse, t
* console.log(`Debug trace saved to: ${result.outputPath}`);
* console.log(`Trace content: ${result.traceContent}`);
*/
export async function writeAVMDebugTrace(input: AVMTracesEventData): Promise<void> {
export async function writeAVMDebugTrace(input: AVMTracesEventData, bufferSizeMb: number): Promise<void> {
try {
const simulateResponse = input.simulateResponse
const projectRoot = await getProjectRoot()
Expand All @@ -71,6 +91,9 @@ export async function writeAVMDebugTrace(input: AVMTracesEventData): Promise<voi
const filename = generateDebugTraceFilename(simulateResponse, timestamp)
const outputFilePath = joinPaths(outputRootDir, filename)

await createDirForFilePathIfNotExists(outputFilePath)
await cleanupOldFiles(bufferSizeMb, outputRootDir)

await writeToFile(outputFilePath, JSON.stringify(simulateResponse.get_obj_for_encoding(), null, 2))
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error))
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { writeAVMDebugTrace, writeTealDebugSourceMaps } from './debugging'
*/
const registerDebugEventHandlers = (): void => {
Config.events.on(EventType.TxnGroupSimulated, async (eventData: AVMTracesEventData) => {
await writeAVMDebugTrace(eventData)
await writeAVMDebugTrace(eventData, Config.traceBufferSizeMb || 256)
})
Config.events.on(EventType.AppCompiled, async (data: TealSourcesDebugEventData) => {
await writeTealDebugSourceMaps(data)
Expand Down
41 changes: 41 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { Config } from '@algorandfoundation/algokit-utils'
import { DEFAULT_MAX_SEARCH_DEPTH } from './constants'

interface ErrnoException extends Error {
errno?: number
code?: string
path?: string
syscall?: string
}

export const isNode = () => {
return typeof process !== 'undefined' && process.versions != null && process.versions.node != null
}
Expand All @@ -13,6 +20,23 @@ export async function writeToFile(filePath: string, content: string): Promise<vo
await fs.promises.writeFile(filePath, content, 'utf8')
}

export async function createDirForFilePathIfNotExists(filePath: string): Promise<void> {
const path = await import('path')
const fs = await import('fs')

try {
await fs.promises.access(path.dirname(filePath))
} catch (error: unknown) {
const err = error as ErrnoException

if (err.code === 'ENOENT') {
await fs.promises.mkdir(path.dirname(filePath), { recursive: true })
} else {
throw err
}
}
}

export async function getProjectRoot(): Promise<string> {
const projectRoot = Config.projectRoot

Expand Down Expand Up @@ -52,3 +76,20 @@ export function joinPaths(...parts: string[]): string {
const separator = typeof process !== 'undefined' && process.platform === 'win32' ? '\\' : '/'
return parts.join(separator).replace(/\/+/g, separator)
}

/**
* Formats a date to YYYYMMDD_HHMMSS in UTC, equivalent to algokit-utils-py format:
* datetime.now(tz=timezone.utc).strftime("%Y%m%d_%H%M%S")
*/
export function formatTimestampUTC(date: Date): string {
// Get UTC components
const year = date.getUTCFullYear()
const month = String(date.getUTCMonth() + 1).padStart(2, '0') // Months are zero-based
const day = String(date.getUTCDate()).padStart(2, '0')
const hours = String(date.getUTCHours()).padStart(2, '0')
const minutes = String(date.getUTCMinutes()).padStart(2, '0')
const seconds = String(date.getUTCSeconds()).padStart(2, '0')

// Format the datetime string
return `${year}${month}${day}_${hours}${minutes}${seconds}`
}

0 comments on commit 3704e6a

Please sign in to comment.