Skip to content
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
51 changes: 51 additions & 0 deletions .cursor/skills/memory-snapshot-report/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
name: memory-snapshot-report
description: Generate and view Unity memory snapshot reports. Use when the user wants to analyze a Unity memory snapshot, export it to a database, or generate/view an HTML report.
---

# Memory Snapshot Report

## When to use

- User wants to analyze a Unity memory snapshot (`.snap` file).
- User wants to export a snapshot to a DuckDB or SQLite database.
- User wants to generate or view an HTML report from an exported snapshot database.

## Prerequisites

- .NET 10 SDK.
- Project path: **MemorySnapshotDataTools** is the project root; run commands from that directory.

## Steps

### 1. Export snapshot to database

From the MemorySnapshotDataTools directory:

```bash
dotnet run --project Cli/MemorySnapshotDataTools.Cli.csproj -- export <path/to/snapshot.snap> <path/to/output.duckdb> --validate minimal --verbose
```

- Use `.duckdb` for DuckDB (recommended) or `.db` for SQLite.
- For SQLite add `--destination sqlite`.
- `--verbose` prints progress and timings (parse+extract vs. write).

### 2. Generate HTML report

```bash
dotnet run --project Cli/MemorySnapshotDataTools.Cli.csproj -- report <path/to/output.duckdb> --out report.html --verbose
```

- Omit `--out` to write to a temp file and open in the browser.
- Use `--title "My Report"` to set the report title.
- Report works with either DuckDB or SQLite databases produced by the export command.

### 3. Optional

- Open the generated HTML file or DB in the user’s preferred viewer.
- For ad-hoc SQL, use the same DB path; tables include `snapshot_info`, `native_objects`, `managed_objects`, `connections`, `native_roots`, `memory_regions`, `native_allocations`.

## Domain

- The tool supports **DuckDB** (default) and **SQLite**; report can be generated from either.
- The CLI reports **timings**: export shows parse+extract vs. write; report shows query vs. render vs. write. Use `--verbose` to see them.
30 changes: 30 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Build and run unit tests on PRs targeting main and on pushes to main.

name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Restore
run: dotnet restore MemorySnapshotDataTools.sln

- name: Build
run: dotnet build MemorySnapshotDataTools.sln -c Release --no-restore

- name: Test
run: dotnet test MemorySnapshotDataTools.sln -c Release --no-build -v normal
25 changes: 25 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Build / output
bin/
obj/
artifacts/
publish/

# IDE
*.user
*.suo
.vs/
.idea/

# OS
.DS_Store
Thumbs.db

# Report artifacts
*.log
*.duckdb
*.db
*.html

# Snapshots
*.snap
MemoryCaptures/
53 changes: 53 additions & 0 deletions Cli/CliOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using MemorySnapshotDataTools;

namespace MemorySnapshotDataTools.Cli;

internal enum CommandKind
{
Export,
Report,
}

/// <summary>
/// Parsed CLI options passed from System.CommandLine handlers to RunExport/RunReport.
/// </summary>
internal sealed class CliOptions
{
public CommandKind Command { get; set; } = CommandKind.Export;
public string SnapshotPath { get; set; } = string.Empty;
public string OutputDbPath { get; set; } = string.Empty;
public string ReportDbPath { get; set; } = string.Empty;
public string? ReportOutputPath { get; set; }
public string ReportTitle { get; set; } = "Memory Snapshot Report";
public int BatchSize { get; set; } = 2048;
public int QueueCapacity { get; set; } = 256;
public ValidationMode Validate { get; set; } = ValidationMode.Minimal;
public DestinationKind Destination { get; set; } = DestinationKind.DuckDb;
public bool Verbose { get; set; }
}

internal sealed class ConsoleProgress : IProgressReporter
{
private readonly bool _verbose;
private readonly object _lock = new();
private DateTime _lastWrite = DateTime.MinValue;

public ConsoleProgress(bool verbose)
{
_verbose = verbose;
}

public void Report(string message, bool force = false)
{
if (!_verbose && !force)
return;

lock (_lock)
{
if (!force && DateTime.UtcNow - _lastWrite < TimeSpan.FromMilliseconds(250))
return;
_lastWrite = DateTime.UtcNow;
Console.WriteLine($"[{DateTime.UtcNow:O}] {message}");
}
}
}
163 changes: 163 additions & 0 deletions Cli/CommandLineBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
using System.CommandLine;
using MemorySnapshotDataTools;

namespace MemorySnapshotDataTools.Cli;

/// <summary>
/// Builds the root command and subcommands (export, report) using System.CommandLine.
/// </summary>
internal static class CommandLineBuilder
{
public static RootCommand Build(Func<CliOptions, int> runExport, Func<CliOptions, int> runReport)
{
var root = new RootCommand("Export Unity memory snapshots to DuckDB or SQLite and generate HTML reports.");

// ---- export ----
var exportCmd = new Command("export", "Export a .snap file to a DuckDB or SQLite database.");
var snapshotArg = new Argument<string>("snapshot")
{
Description = "Path to the Unity memory snapshot (.snap) file.",
Arity = ArgumentArity.ExactlyOne,
};
var outputArg = new Argument<string>("output")
{
Description = "Path to the output database (.duckdb or .db).",
Arity = ArgumentArity.ExactlyOne,
};
exportCmd.Add(snapshotArg);
exportCmd.Add(outputArg);

var batchSizeOpt = new Option<int>("--batch-size")
{
Description = "Rows per produced batch.",
DefaultValueFactory = _ => 2048,
};
var queueCapacityOpt = new Option<int>("--queue-capacity")
{
Description = "Max queued batches.",
DefaultValueFactory = _ => 256,
};
var validateOpt = new Option<string>("--validate")
{
Description = "Validation mode: none, minimal, or full.",
DefaultValueFactory = _ => "minimal",
};
validateOpt.AcceptOnlyFromAmong("none", "minimal", "full");
var destinationOpt = new Option<string>("--destination")
{
Description = "Export backend: duckdb or sqlite.",
DefaultValueFactory = _ => "duckdb",
};
destinationOpt.AcceptOnlyFromAmong("duckdb", "sqlite");
var verboseOpt = new Option<bool>("--verbose")
{
Description = "Print progress updates.",
};

exportCmd.Add(batchSizeOpt);
exportCmd.Add(queueCapacityOpt);
exportCmd.Add(validateOpt);
exportCmd.Add(destinationOpt);
exportCmd.Add(verboseOpt);

exportCmd.SetAction((ParseResult parseResult) =>
{
var snapshotPath = ExpandPath(parseResult.GetValue(snapshotArg)!);
var outputDbPath = ExpandPath(parseResult.GetValue(outputArg)!);
if (!File.Exists(snapshotPath))
{
Console.Error.WriteLine($"Snapshot file not found: {snapshotPath}");
return 1;
}
var options = new CliOptions
{
Command = CommandKind.Export,
SnapshotPath = snapshotPath,
OutputDbPath = outputDbPath,
BatchSize = parseResult.GetValue(batchSizeOpt),
QueueCapacity = parseResult.GetValue(queueCapacityOpt),
Validate = ParseValidationMode(parseResult.GetValue(validateOpt)!),
Destination = parseResult.GetValue(destinationOpt)!.ToLowerInvariant() == "sqlite" ? DestinationKind.Sqlite : DestinationKind.DuckDb,
Verbose = parseResult.GetValue(verboseOpt),
};
return runExport(options);
});

// ---- report ----
var reportCmd = new Command("report", "Generate an HTML report from an exported database.");
var databaseArg = new Argument<string>("database")
{
Description = "Path to the exported database (.duckdb or .db).",
Arity = ArgumentArity.ExactlyOne,
};
reportCmd.Add(databaseArg);

var outOpt = new Option<string?>("--out")
{
Description = "Output HTML file path (default: temp file + open in browser).",
};
var titleOpt = new Option<string>("--title")
{
Description = "Report title.",
DefaultValueFactory = _ => "Memory Snapshot Report",
};
var reportVerboseOpt = new Option<bool>("--verbose")
{
Description = "Print progress and timings.",
};

reportCmd.Add(outOpt);
reportCmd.Add(titleOpt);
reportCmd.Add(reportVerboseOpt);

reportCmd.SetAction((ParseResult parseResult) =>
{
var reportDbPath = ExpandPath(parseResult.GetValue(databaseArg)!);
if (!File.Exists(reportDbPath))
{
Console.Error.WriteLine($"Database file not found: {reportDbPath}");
return 1;
}
var outPath = parseResult.GetValue(outOpt);
var options = new CliOptions
{
Command = CommandKind.Report,
ReportDbPath = reportDbPath,
ReportOutputPath = string.IsNullOrWhiteSpace(outPath) ? null : ExpandPath(outPath),
ReportTitle = parseResult.GetValue(titleOpt)!,
Verbose = parseResult.GetValue(reportVerboseOpt),
};
return runReport(options);
});

root.Add(exportCmd);
root.Add(reportCmd);
return root;
}

private static ValidationMode ParseValidationMode(string value)
{
return value.ToLowerInvariant() switch
{
"none" => ValidationMode.None,
"minimal" => ValidationMode.Minimal,
"full" => ValidationMode.Full,
_ => ValidationMode.Minimal,
};
}

private static string ExpandPath(string value)
{
if (string.IsNullOrWhiteSpace(value))
return value;

var expanded = Environment.ExpandEnvironmentVariables(value);
if (expanded.StartsWith("~/", StringComparison.Ordinal) || expanded == "~")
{
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var suffix = expanded.Length > 2 ? expanded[2..] : string.Empty;
expanded = Path.Combine(home, suffix);
}
return Path.GetFullPath(expanded);
}
}
19 changes: 19 additions & 0 deletions Cli/MemorySnapshotDataTools.Cli.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<RootNamespace>MemorySnapshotDataTools.Cli</RootNamespace>
<AssemblyName>MemorySnapshotDataTools</AssemblyName>
<Version>0.1.0</Version>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.3" />
<ProjectReference Include="..\Core\MemorySnapshotDataTools.Core.csproj" />
</ItemGroup>
</Project>
Loading
Loading