diff options
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/release.sh | 508 | ||||
| -rwxr-xr-x | scripts/span-table.ts | 420 |
2 files changed, 0 insertions, 928 deletions
diff --git a/scripts/release.sh b/scripts/release.sh deleted file mode 100755 index 8e0717a2..00000000 --- a/scripts/release.sh +++ /dev/null @@ -1,508 +0,0 @@ -#!/usr/bin/env bash -# -# Atuin CLI Release Script -# -# But first โ make a cup of tea. Releases without tea are a crime. ๐ซ -# - -set -euo pipefail - -WORKDIR="" - -cleanup_on_error() { - if [[ $? -ne 0 && -n "$WORKDIR" ]]; then - echo "" - echo -e " \033[0;31mโ\033[0m Script failed. Working directory preserved at:" - echo -e " \033[2m$WORKDIR\033[0m" - fi -} -trap cleanup_on_error EXIT - -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -# Formatting -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -MAGENTA='\033[0;35m' -BOLD='\033[1m' -DIM='\033[2m' -NC='\033[0m' - -info() { echo -e " ${BLUE}โธ${NC} $*"; } -success() { echo -e " ${GREEN}โ${NC} $*"; } -warn() { echo -e " ${YELLOW}โ ${NC} $*"; } -die() { echo -e " ${RED}โ${NC} $*" >&2; exit 1; } - -step() { - echo "" - echo -e " ${MAGENTA}${BOLD}โโโ $* โโโ${NC}" - echo "" -} - -confirm() { - echo -en " ${CYAN}โธ${NC} $1 ${DIM}[y/N]${NC} " - read -r reply - [[ "$reply" =~ ^[Yy]$ ]] -} - -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -# Banner -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ - -banner() { - echo "" - echo -e "${CYAN}" - cat <<'BANNER' - - : - =#*#= - #*+*#. - #**++*+ - =#*++++#. - **+++++** - :#++++++*#. - **+++++++*# - -#*+++++++*#- - ##*****#####* - -*##########:.+####*. - ***********####*****+ - :**#############*#+ - #+----------------*- - -*###########**+++##-----=##=-----=##=# - .#*-==.. =*------------------* - -#-::::::*=----------#*---------=----=--=* - +*:::::::--:::::::::::**---+#=----===---=* - #*::::::::=::::::::::::**-----+#*=-----+#- - .#+::::::::=-::::::::::::**--------=*#### - #=::::::::=*:::::::::::::+*---------=#-*#= - ##-:::-**+=+*-:::::::::::-#=--------=#=##- - :#+**+========++=::::-=++=#+--------+#+#* - +##++============+*#+=====*+--------+#*# - *=*#*+==+**++++*#++*====+###--------*###* - *=-*##=+#=-------#**===+#+---------=##*=# - *=-=*#**+---------#*+==+#----------*##--# - :#*- ###=---------#+*==+#=-------=###=-=# - +=---------#+#+++#*++++*##*== .. # - ...... +=--*++*+=-#####+-. -+****=. - ....... =*== . + ........... - ............::-:............................ - ............................................ - ....###....########.##.....##.####.##....##. - ...##.##......##....##.....##..##..###...##. - ..##...##.....##....##.....##..##..####..##. - .##.....##....##....##.....##..##..##.##.##. - .#########....##....##.....##..##..##..####. - .##.....##....##....##.....##..##..##...###. - .##.....##....##.....#######..####.##....##. - ............................................ - - ~ Magical Shell History ~ - Release Script - -BANNER - echo -e "${NC}" - echo "" -} - -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -# 1. Dependency check -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ - -check_deps() { - step "Dependencies" - - local missing=() - local deps=(git gsed cargo gh git-cliff) - - for cmd in "${deps[@]}"; do - if command -v "$cmd" &>/dev/null; then - success "${BOLD}$cmd${NC} ${DIM}$(command -v "$cmd")${NC}" - else - echo -e " ${RED}โ${NC} ${BOLD}$cmd${NC} not found" - missing+=("$cmd") - fi - done - - if (( ${#missing[@]} )); then - echo "" - die "Missing required tools: ${missing[*]}\n Install with: ${DIM}brew install ${missing[*]}${NC}" - fi -} - -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -# 2. Clone into working directory -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ - -setup_workdir() { - step "Working Directory" - - WORKDIR=$(mktemp -d) - info "Created ${DIM}$WORKDIR${NC}" - - info "Cloning atuinsh/atuin..." - git clone --quiet git@github.com:atuinsh/atuin.git "$WORKDIR" - cd "$WORKDIR" - success "Repository cloned" -} - -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -# 3. Version -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ - -detect_current_version() { - sed -n '/^\[workspace\.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml -} - -get_version() { - step "Version" - - CURRENT_VERSION=$(detect_current_version) - info "Current version: ${BOLD}$CURRENT_VERSION${NC}" - - # Suggest the next version based on conventional commits - local suggested - suggested=$(git-cliff --bumped-version 2>/dev/null | sed 's/^v//' || true) - if [[ -n "$suggested" && "$suggested" != "$CURRENT_VERSION" ]]; then - info "Suggested next: ${BOLD}$suggested${NC} ${DIM}(based on conventional commits)${NC}" - fi - - echo "" - - if [[ -n "${NEW_VERSION:-}" ]]; then - info "Using version from environment: ${BOLD}$NEW_VERSION${NC}" - else - echo -en " ${CYAN}โธ${NC} New version ${DIM}(without 'v' prefix)${NC}: " - read -r NEW_VERSION - fi - - [[ -n "$NEW_VERSION" ]] || die "Version cannot be empty" - - IS_PRERELEASE=false - if [[ "$NEW_VERSION" == *-* ]]; then - IS_PRERELEASE=true - warn "Pre-release detected" - fi - - echo "" - info "${BOLD}$CURRENT_VERSION${NC} โ ${BOLD}$NEW_VERSION${NC}" - echo "" - confirm "Proceed with release?" || { info "Aborted."; exit 0; } -} - -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -# 4. Update version numbers -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ - -update_versions() { - step "Updating Versions" - - info "Creating release branch: ${BOLD}$NEW_VERSION${NC}" - git checkout -b "$NEW_VERSION" --quiet - - local version_pattern="${CURRENT_VERSION//./\\.}" - - info "Replacing ${DIM}$CURRENT_VERSION${NC} โ ${DIM}$NEW_VERSION${NC} in Cargo.toml files..." - find . -type f -name 'Cargo.toml' -not -path './.git/*' \ - -exec gsed -i "s/$version_pattern/$NEW_VERSION/g" {} \; - - info "Running ${DIM}cargo check${NC} to update Cargo.lock (this may take a moment)..." - cargo check --quiet 2>&1 || cargo check - - echo "" - info "Changed files:" - git diff --stat | sed 's/^/ /' - - echo "" - - # Verify the workspace version was updated - local new_ws_version - new_ws_version=$(detect_current_version) - if [[ "$new_ws_version" == "$NEW_VERSION" ]]; then - success "Workspace version updated" - else - die "Workspace version is '$new_ws_version', expected '$NEW_VERSION'" - fi - - # Verify we didn't break anything unexpected โ show version-related - # lines in the diff for review - echo "" - info "Version changes in diff:" - git diff --unified=0 -- '*.toml' \ - | grep -E '^\+.*version' \ - | grep -v '^\+\+\+' \ - | sed 's/^/ /' || true - echo "" - - confirm "Version changes look correct?" || { die "Aborting โ fix versions manually in $WORKDIR"; } -} - -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -# 5. Changelog -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ - -update_changelog() { - step "Changelog" - - # cliff.toml's ignore_tags already ignores beta/alpha tags, so - # --unreleased always gives us everything since the last stable release. - # - # Prereleases: heading is ## [unreleased] (running tally) - # Stable: heading is ## X.Y.Z (versioned entry) - local cliff_args=(--unreleased --strip all) - - if $IS_PRERELEASE; then - info "Updating ${BOLD}Unreleased${NC} section..." - else - cliff_args+=(--tag "v$NEW_VERSION") - info "Generating changelog for ${BOLD}$NEW_VERSION${NC}..." - fi - - local new_entry - new_entry=$(git-cliff "${cliff_args[@]}" 2>/dev/null || true) - - # Check if the entry is empty (just a heading with no content) - if [[ -z "$new_entry" ]] || [[ "$(echo "$new_entry" | grep -c '[a-zA-Z]')" -le 1 ]]; then - warn "No unreleased changes detected by git-cliff" - warn "You may want to add entries manually in the editor" - if $IS_PRERELEASE; then - new_entry="## [unreleased]" - else - new_entry="## $NEW_VERSION" - fi - else - local commit_count - commit_count=$(echo "$new_entry" | grep -c '^- ' || true) - success "Generated entry with ${BOLD}$commit_count${NC} item(s)" - fi - - # Remove any existing [unreleased] section โ we'll replace it with - # either an updated unreleased section or a versioned one - if grep -qi '^\## \[unreleased\]' CHANGELOG.md; then - info "Removing old Unreleased section..." - awk ' - /^## \[[Uu]nreleased\]/ { skip=1; next } - /^## / { skip=0 } - !skip - ' CHANGELOG.md > CHANGELOG.md.tmp - mv CHANGELOG.md.tmp CHANGELOG.md - fi - - # Insert the new entry before the first existing version heading - local insert_line - insert_line=$(grep -n '^## ' CHANGELOG.md | head -1 | cut -d: -f1) - - if [[ -n "$insert_line" ]]; then - { - head -n "$((insert_line - 1))" CHANGELOG.md - echo "$new_entry" - echo "" - echo "" - tail -n "+$insert_line" CHANGELOG.md - } > CHANGELOG.md.tmp - mv CHANGELOG.md.tmp CHANGELOG.md - else - warn "No existing version headings found โ appending to end" - echo "" >> CHANGELOG.md - echo "$new_entry" >> CHANGELOG.md - fi - - echo "" - info "Opening CHANGELOG.md in your editor for review..." - info "${DIM}Verify the entry, make any edits, then save and close.${NC}" - echo "" - echo -en " ${DIM}Press Enter to open editor...${NC}" - read -r - "${EDITOR:-${VISUAL:-vi}}" CHANGELOG.md - success "Changelog finalized" -} - -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -# 6. Commit and push -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ - -commit_and_push() { - step "Commit & Push" - - git add . - git commit --quiet -m "chore(release): prepare for release $NEW_VERSION" - success "Committed" - - info "Pushing branch..." - git push --quiet --set-upstream origin "$(git branch --show-current)" 2>&1 - success "Pushed to origin/${BOLD}$NEW_VERSION${NC}" -} - -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -# 7. Pull request -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ - -extract_changelog_entry() { - # Extract the changelog body (without the heading) for the entry we just wrote. - # Prereleases use ## [unreleased], stable uses ## X.Y.Z - local heading - if $IS_PRERELEASE; then - heading='## \[unreleased\]' - else - heading="## ${NEW_VERSION//./\\.}" - fi - awk "/^${heading}/{found=1; next} /^## /{if(found) exit} found" CHANGELOG.md -} - -create_pr() { - step "Pull Request" - - local changelog_body - changelog_body=$(extract_changelog_entry) - - local pr_body - pr_body="Release preparation for v${NEW_VERSION}." - if [[ -n "$changelog_body" ]]; then - pr_body="$(cat <<EOF -Release preparation for v${NEW_VERSION}. - -## Changelog - -$changelog_body -EOF -)" - fi - - info "Creating PR..." - PR_URL=$(gh pr create \ - --title "chore(release): prepare for release $NEW_VERSION" \ - --body "$pr_body" \ - --repo atuinsh/atuin) - - success "PR created: ${BOLD}$PR_URL${NC}" - - local pr_number - pr_number=$(echo "$PR_URL" | grep -o '[0-9]*$') - - echo "" - info "Waiting for PR #${BOLD}$pr_number${NC} to be merged..." - info "${DIM}Review and merge the PR โ this script will detect it automatically.${NC}" - echo "" - - while true; do - local state - state=$(gh pr view "$pr_number" --repo atuinsh/atuin --json state --jq '.state' 2>/dev/null || echo "UNKNOWN") - - case "$state" in - MERGED) - echo "" - success "PR #$pr_number merged!" - break - ;; - CLOSED) - echo "" - die "PR #$pr_number was closed without merging" - ;; - *) - printf "\r ${DIM}โณ PR #%s is %s โ checking again in 5s...${NC} " "$pr_number" "$state" - sleep 5 - ;; - esac - done -} - -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -# 8. Tag -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ - -tag_release() { - step "Tag & Release" - - info "Switching to main and pulling..." - git checkout main --quiet - git pull --quiet - - info "Creating tag ${BOLD}v$NEW_VERSION${NC}" - git tag "v$NEW_VERSION" - - info "Pushing tag..." - git push --tags - success "Tag ${BOLD}v$NEW_VERSION${NC} pushed โ release workflow triggered" -} - -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -# 9. Publish to crates.io -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ - -publish_crates() { - step "Publish to crates.io" - - if $IS_PRERELEASE; then - warn "Pre-release โ skipping crates.io publish" - return - fi - - if ! confirm "Publish to crates.io?"; then - info "Skipping" - return - fi - - local crates=( - atuin-common - atuin-client - atuin-ai - atuin-dotfiles - atuin-history - atuin-nucleo/matcher - atuin-nucleo - atuin-daemon - atuin-kv - atuin-scripts - atuin-server-database - atuin-server-postgres - atuin-server-sqlite - atuin-server - atuin-pty-proxy - atuin - ) - - for crate in "${crates[@]}"; do - info "Publishing ${BOLD}$crate${NC}..." - local output - # --no-verify: skip rebuild during publish โ the code already passed - # CI, and verification fails on workspace crates whose freshly-published - # dependencies haven't been indexed by crates.io yet - if output=$(cd "crates/$crate" && cargo publish --no-verify 2>&1); then - success "$crate published" - elif echo "$output" | grep -q "already uploaded"; then - warn "$crate already published, skipping" - else - echo "$output" >&2 - die "Failed to publish $crate" - fi - done -} - -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -# Main -# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ - -main() { - banner - check_deps - setup_workdir - get_version - update_versions - update_changelog - commit_and_push - create_pr - tag_release - publish_crates - - step "Done ๐" - success "Released ${BOLD}v$NEW_VERSION${NC}" - echo "" - info "Working directory: ${DIM}$WORKDIR${NC}" - info "Clean up with: ${DIM}rm -rf $WORKDIR${NC}" - echo "" -} - -main "$@" diff --git a/scripts/span-table.ts b/scripts/span-table.ts deleted file mode 100755 index 3656f129..00000000 --- a/scripts/span-table.ts +++ /dev/null @@ -1,420 +0,0 @@ -#!/usr/bin/env bun -/** - * Analyze span timing JSON logs generated with ATUIN_SPAN - * - * Usage: bun scripts/span-table.ts <file.json> [options] - * --filter <pattern> Only show spans matching pattern (regex) - * --sort <field> Sort by: calls, avg, total, p99 (default: total) - * --top <n> Show top N spans (default: 20) - * --detail <span> Show individual calls for a specific span - * --all Include internal/library spans - */ - -import { readFileSync } from "fs"; - -interface SpanEvent { - timestamp: string; - level: string; - fields: { - message: string; - "time.busy"?: string; - "time.idle"?: string; - }; - target: string; - span?: { - name: string; - [key: string]: unknown; - }; - spans?: Array<{ name: string; [key: string]: unknown }>; -} - -interface SpanStats { - name: string; - calls: number; - busyTimes: number[]; // in microseconds - idleTimes: number[]; - parentCounts: Map<string, number>; // parent span name -> count -} - -// Parse duration strings like "1.23ms", "456ยตs", "789ns" to microseconds -function parseDuration(duration: string): number { - const match = duration.match(/^([\d.]+)(ns|ยตs|us|ms|s)$/); - if (!match) return 0; - - const value = parseFloat(match[1]); - const unit = match[2]; - - switch (unit) { - case "ns": - return value / 1000; - case "ยตs": - case "us": - return value; - case "ms": - return value * 1000; - case "s": - return value * 1_000_000; - default: - return 0; - } -} - -// Format microseconds for display -function formatDuration(us: number): string { - if (us < 1) { - return `${(us * 1000).toFixed(0)}ns`; - } else if (us < 1000) { - return `${us.toFixed(2)}ยตs`; - } else if (us < 1_000_000) { - return `${(us / 1000).toFixed(2)}ms`; - } else { - return `${(us / 1_000_000).toFixed(2)}s`; - } -} - -function percentile(arr: number[], p: number): number { - if (arr.length === 0) return 0; - const sorted = [...arr].sort((a, b) => a - b); - const idx = Math.floor(sorted.length * p); - return sorted[Math.min(idx, sorted.length - 1)]; -} - -function parseJsonLines(content: string): SpanEvent[] { - const events: SpanEvent[] = []; - for (const line of content.trim().split("\n")) { - if (!line.trim()) continue; - try { - events.push(JSON.parse(line)); - } catch { - // Skip malformed lines - } - } - return events; -} - -function main() { - const args = process.argv.slice(2); - - // Parse arguments - let filterPattern: RegExp | null = null; - let sortField = "total"; - let topN = 20; - let detailSpan: string | null = null; - let showAll = false; - const files: string[] = []; - - for (let i = 0; i < args.length; i++) { - if (args[i] === "--filter" && args[i + 1]) { - filterPattern = new RegExp(args[++i]); - } else if (args[i] === "--sort" && args[i + 1]) { - sortField = args[++i]; - } else if (args[i] === "--top" && args[i + 1]) { - topN = parseInt(args[++i], 10); - } else if (args[i] === "--detail" && args[i + 1]) { - detailSpan = args[++i]; - } else if (args[i] === "--all") { - showAll = true; - } else if (!args[i].startsWith("-")) { - files.push(args[i]); - } - } - - if (files.length === 0) { - console.error("Usage: bun scripts/span-table.ts <file.json> [options]"); - console.error(" --filter <pattern> Only show spans matching pattern (regex)"); - console.error(" --sort <field> Sort by: calls, avg, total, p99 (default: total)"); - console.error(" --top <n> Show top N spans (default: 20)"); - console.error(" --detail <span> Show individual calls for a specific span"); - console.error(" --all Include internal/library spans"); - process.exit(1); - } - - // Parse all files - const allEvents: SpanEvent[] = []; - for (const file of files) { - const content = readFileSync(file, "utf-8"); - for (const event of parseJsonLines(content)) { - allEvents.push(event); - } - } - - // Filter to close events and aggregate by span name - const spans = new Map<string, SpanStats>(); - - for (const event of allEvents) { - if (event.fields?.message !== "close") continue; - if (!event.span?.name) continue; - if (!event.fields["time.busy"]) continue; - - const name = event.span.name; - - // Apply filter if specified - if (filterPattern && !filterPattern.test(name)) continue; - - // Skip noisy internal spans unless explicitly requested - if ( - !showAll && - !filterPattern && - !detailSpan && - (name.startsWith("FramedRead::") || - name.startsWith("FramedWrite::") || - name.startsWith("Prioritize::") || - name === "poll" || - name === "poll_ready" || - name === "Connection" || - name.startsWith("assign_") || - name.startsWith("reserve_") || - name.startsWith("try_") || - name.startsWith("send_") || - name.startsWith("pop_")) - ) { - continue; - } - - if (!spans.has(name)) { - spans.set(name, { name, calls: 0, busyTimes: [], idleTimes: [], parentCounts: new Map() }); - } - - const stats = spans.get(name)!; - stats.calls++; - stats.busyTimes.push(parseDuration(event.fields["time.busy"])); - if (event.fields["time.idle"]) { - stats.idleTimes.push(parseDuration(event.fields["time.idle"])); - } - - // Track parent relationship (immediate parent is the last element in spans array) - const parents = event.spans || []; - const parentName = parents.length > 0 ? parents[parents.length - 1].name : "__root__"; - stats.parentCounts.set(parentName, (stats.parentCounts.get(parentName) || 0) + 1); - } - - if (spans.size === 0) { - console.error("No matching span close events found"); - process.exit(1); - } - - // Detail mode: show individual calls for a specific span - if (detailSpan) { - const detailEvents: Array<{ - timestamp: string; - busy: number; - idle: number; - fields: Record<string, unknown>; - parents: string[]; - }> = []; - - for (const event of allEvents) { - if (event.fields?.message !== "close") continue; - if (event.span?.name !== detailSpan) continue; - if (!event.fields["time.busy"]) continue; - - // Extract span fields (excluding name) - const fields: Record<string, unknown> = {}; - if (event.span) { - for (const [k, v] of Object.entries(event.span)) { - if (k !== "name") fields[k] = v; - } - } - - // Get parent span names - const parents = (event.spans || []).map((s) => s.name); - - detailEvents.push({ - timestamp: event.timestamp, - busy: parseDuration(event.fields["time.busy"]), - idle: event.fields["time.idle"] ? parseDuration(event.fields["time.idle"]) : 0, - fields, - parents, - }); - } - - if (detailEvents.length === 0) { - console.error(`No events found for span "${detailSpan}"`); - process.exit(1); - } - - console.log(""); - console.log(`Individual calls for: ${detailSpan}`); - console.log("-".repeat(110)); - console.log( - "#".padStart(4) + - "Wall".padStart(12) + - "Busy".padStart(12) + - "Idle".padStart(12) + - " Fields" - ); - console.log("-".repeat(110)); - - detailEvents.forEach((e, i) => { - const fieldsStr = Object.keys(e.fields).length > 0 - ? JSON.stringify(e.fields) - : ""; - - console.log( - (i + 1).toString().padStart(4) + - formatDuration(e.busy + e.idle).padStart(12) + - formatDuration(e.busy).padStart(12) + - formatDuration(e.idle).padStart(12) + - " " + - fieldsStr - ); - }); - - // Summary stats - const busyTimes = detailEvents.map((e) => e.busy); - const wallTimes = detailEvents.map((e) => e.busy + e.idle); - console.log(""); - console.log( - `Summary: ${detailEvents.length} calls\n` + - ` Wall: avg=${formatDuration(wallTimes.reduce((a, b) => a + b, 0) / wallTimes.length)}, ` + - `min=${formatDuration(Math.min(...wallTimes))}, ` + - `max=${formatDuration(Math.max(...wallTimes))}, ` + - `p50=${formatDuration(percentile(wallTimes, 0.5))}, ` + - `p99=${formatDuration(percentile(wallTimes, 0.99))}\n` + - ` Busy: avg=${formatDuration(busyTimes.reduce((a, b) => a + b, 0) / busyTimes.length)}, ` + - `min=${formatDuration(Math.min(...busyTimes))}, ` + - `max=${formatDuration(Math.max(...busyTimes))}, ` + - `p50=${formatDuration(percentile(busyTimes, 0.5))}, ` + - `p99=${formatDuration(percentile(busyTimes, 0.99))}` - ); - return; - } - - // Calculate stats - const results = [...spans.values()].map((s) => { - // Calculate wall times (busy + idle) for each call - const wallTimes = s.busyTimes.map((busy, i) => busy + (s.idleTimes[i] || 0)); - - // Find most common parent - let mostCommonParent = "__root__"; - let maxCount = 0; - for (const [parent, count] of s.parentCounts) { - if (count > maxCount) { - maxCount = count; - mostCommonParent = parent; - } - } - - return { - name: s.name, - calls: s.calls, - total: s.busyTimes.reduce((a, b) => a + b, 0), - avg: s.busyTimes.reduce((a, b) => a + b, 0) / s.calls, - min: Math.min(...s.busyTimes), - max: Math.max(...s.busyTimes), - p50: percentile(s.busyTimes, 0.5), - p99: percentile(s.busyTimes, 0.99), - avgWall: wallTimes.reduce((a, b) => a + b, 0) / s.calls, - p50Wall: percentile(wallTimes, 0.5), - p99Wall: percentile(wallTimes, 0.99), - parent: mostCommonParent, - }; - }); - - // Build tree structure - const childrenOf = new Map<string, string[]>(); - childrenOf.set("__root__", []); - for (const r of results) { - if (!childrenOf.has(r.name)) { - childrenOf.set(r.name, []); - } - if (!childrenOf.has(r.parent)) { - childrenOf.set(r.parent, []); - } - childrenOf.get(r.parent)!.push(r.name); - } - - // Sort children by the specified field - const resultMap = new Map(results.map(r => [r.name, r])); - const sortChildren = (children: string[]) => { - children.sort((a, b) => { - const ra = resultMap.get(a); - const rb = resultMap.get(b); - if (!ra || !rb) return 0; - switch (sortField) { - case "calls": - return rb.calls - ra.calls; - case "avg": - return rb.avg - ra.avg; - case "p99": - return rb.p99 - ra.p99; - case "total": - default: - return rb.total - ra.total; - } - }); - }; - - // Traverse tree to build ordered display list with depths - const displayResults: Array<{ result: typeof results[0]; depth: number }> = []; - const visited = new Set<string>(); - - function traverse(name: string, depth: number) { - if (visited.has(name)) return; - visited.add(name); - - const result = resultMap.get(name); - if (result) { - displayResults.push({ result, depth }); - } - - const children = childrenOf.get(name) || []; - sortChildren(children); - for (const child of children) { - traverse(child, depth + 1); - } - } - - // Start from roots - const roots = childrenOf.get("__root__") || []; - sortChildren(roots); - for (const root of roots) { - traverse(root, 0); - } - - // Add any orphaned spans (whose parent wasn't in our span list) - for (const r of results) { - if (!visited.has(r.name)) { - displayResults.push({ result: r, depth: 0 }); - } - } - - // Apply topN limit - const limitedResults = displayResults.slice(0, topN); - - console.log(""); - console.log( - "Span Name".padEnd(40) + - "Calls".padStart(6) + - "Avg(wall)".padStart(11) + - "P50(wall)".padStart(11) + - "P99(wall)".padStart(11) + - "Avg(busy)".padStart(11) + - "P50(busy)".padStart(11) + - "P99(busy)".padStart(11) - ); - console.log("-".repeat(112)); - - for (const { result: r, depth } of limitedResults) { - const indent = " ".repeat(depth); - const maxNameLen = 38 - indent.length; - const truncatedName = r.name.length > maxNameLen ? "..." + r.name.slice(-(maxNameLen - 3)) : r.name; - const displayName = indent + truncatedName; - - console.log( - displayName.padEnd(40) + - r.calls.toString().padStart(6) + - formatDuration(r.avgWall).padStart(11) + - formatDuration(r.p50Wall).padStart(11) + - formatDuration(r.p99Wall).padStart(11) + - formatDuration(r.avg).padStart(11) + - formatDuration(r.p50).padStart(11) + - formatDuration(r.p99).padStart(11) - ); - } - - console.log(""); - console.log(`Showing ${limitedResults.length} of ${results.length} spans (sorted by ${sortField})`); -} - -main(); |
