Skip to content

Commit

Permalink
Merge pull request #18 from algorandfoundation/fix/trace-filename-gen…
Browse files Browse the repository at this point in the history
…eration

fix: filename generation and match timestamp format on utils-py
  • Loading branch information
neilcampbell authored Nov 8, 2024
2 parents 605d0ca + 4d34297 commit 1373d56
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 20 deletions.
6 changes: 4 additions & 2 deletions docs/code/functions/writeAVMDebugTrace.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

# Function: writeAVMDebugTrace()

> **writeAVMDebugTrace**(`input`): `Promise`\<`void`\>
> **writeAVMDebugTrace**(`input`, `bufferSizeMb`): `Promise`\<`void`\>
Generates an AVM debug trace from the provided simulation response and persists it to a file.

Expand All @@ -16,6 +16,8 @@ Generates an AVM debug trace from the provided simulation response and persists

The AVMTracesEventData containing the simulation response and other relevant information.

**bufferSizeMb**: `number`

## Returns

`Promise`\<`void`\>
Expand All @@ -36,4 +38,4 @@ console.log(`Trace content: ${result.traceContent}`);

## Defined in

[debugging/writeAVMDebugTrace.ts:20](https://github.com/algorandfoundation/algokit-utils-ts-debug/blob/main/src/debugging/writeAVMDebugTrace.ts#L20)
[debugging/writeAVMDebugTrace.ts:85](https://github.com/algorandfoundation/algokit-utils-ts-debug/blob/main/src/debugging/writeAVMDebugTrace.ts#L85)
96 changes: 95 additions & 1 deletion src/debugging/writeAVMDebugTrace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { Config, EventType, performAtomicTransactionComposerSimulate } from '@al
import { algorandFixture } from '@algorandfoundation/algokit-utils/testing'
import { describe, expect, test } from '@jest/globals'
import algosdk, { makeEmptyTransactionSigner } from 'algosdk'
import { SimulateResponse } from 'algosdk/dist/types/client/v2/algod/models/types'
import * as fs from 'fs/promises'
import * as os from 'os'
import * as path from 'path'
import { DEBUG_TRACES_DIR } from '../constants'
import { registerDebugEventHandlers } from '../index'
import { cleanupOldFiles, generateDebugTraceFilename } from './writeAVMDebugTrace'

describe('simulateAndPersistResponse tests', () => {
describe('writeAVMDebugTrace tests', () => {
const localnet = algorandFixture()

beforeAll(async () => {
Expand Down Expand Up @@ -45,3 +47,95 @@ describe('simulateAndPersistResponse tests', () => {
jest.restoreAllMocks()
})
})

describe('generateDebugTraceFilename', () => {
const TEST_CASES: Array<[string, object, string]> = [
[
'single payment transaction',
{
lastRound: 1000,
txnGroups: [
{
txnResults: [{ txnResult: { txn: { txn: { type: 'pay' } } } }],
},
],
},
'1pay',
],
[
'multiple transaction types',
{
lastRound: 1000,
txnGroups: [
{
txnResults: [
{ txnResult: { txn: { txn: { type: 'pay' } } } },
{ txnResult: { txn: { txn: { type: 'pay' } } } },
{ txnResult: { txn: { txn: { type: 'axfer' } } } },
{ txnResult: { txn: { txn: { type: 'appl' } } } },
{ txnResult: { txn: { txn: { type: 'appl' } } } },
{ txnResult: { txn: { txn: { type: 'appl' } } } },
],
},
],
},
'2pay_1axfer_3appl',
],
]

test.each(TEST_CASES)('%s', (testName, mockResponse, expectedPattern) => {
const timestamp = '20230101_120000'
const filename = generateDebugTraceFilename(mockResponse as SimulateResponse, timestamp)
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)
})
})
89 changes: 73 additions & 16 deletions src/debugging/writeAVMDebugTrace.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,71 @@
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
}

/**
* Removes old trace files when total size exceeds buffer limit
*/
export async function cleanupOldFiles(bufferSizeMb: number, outputRootDir: string): Promise<void> {
const fs = await import('fs')
const path = await import('path')

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) => {
txnGroup.txnResults.forEach(({ txnResult }) => {
const { type } = txnResult.txn.txn
if (!acc.has(type)) {
acc.set(type, { type, count: 0 })
}
const entry = acc.get(type)!
entry.count++
})
return acc
}, new Map())

const txnTypesStr = Array.from(txnTypesCount.values())
.map(({ count, type }) => `${count}${type}`)
.join('_')

const lastRound = simulateResponse.lastRound
return `${timestamp}_lr${lastRound}_${txnTypesStr}.trace.avm.json`
}

/**
* Generates an AVM debug trace from the provided simulation response and persists it to a file.
Expand All @@ -17,25 +82,17 @@ import { getProjectRoot, joinPaths, writeToFile } from '../utils'
* 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 txnGroups = simulateResponse.txnGroups
const projectRoot = await getProjectRoot()

const txnTypesCount = txnGroups.reduce((acc: Record<string, number>, txnGroup) => {
const txnType = txnGroup.txnResults[0].txnResult.txn.txn.type
acc[txnType] = (acc[txnType] || 0) + 1
return acc
}, {})

const txnTypesStr = Object.entries(txnTypesCount)
.map(([type, count]) => `${count}${type}`)
.join('_')

const timestamp = new Date().toISOString().replace(/[:.]/g, '')
const timestamp = formatTimestampUTC(new Date())
const outputRootDir = joinPaths(projectRoot, DEBUG_TRACES_DIR)
const outputFilePath = joinPaths(outputRootDir, `${timestamp}_${txnTypesStr}.trace.avm.json`)
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) {
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 1373d56

Please sign in to comment.