Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: filename generation and match timestamp format on utils-py #18

Merged
merged 2 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}`
}
Loading