Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Catalog · How the catalog works
Anything · Lazarus or Delphi
Random projectRandom project

MapAnalyzer

Delphi map file analyzer

View on GitHub

Member rating

No ratings yet.

Sign in to rate this project.

README

MapAnalyzer

A single-file Delphi console app that analyses .map files. No dependencies — SysUtils, Classes, Math only, so it builds on old Delphi versions (Delphi 6/7 and up — pointer-sized integers are shimmed for pre-2009 compilers) as well as current ones, and on FPC.

pascal
dcc32 MapAnalyzer.dpr          # Win32
dcc64 MapAnalyzer.dpr          # Win64
fpc -Mdelphi MapAnalyzer.dpr   # if you want it outside the IDE

Prerequisite

Project ▸ Options ▸ Building ▸ Delphi Compiler ▸ Linking ▸ Map file = Detailed. The lines command additionally needs debug information enabled.

Start here

pascal
MapAnalyzer sections MyApp.map

Prints what the parser recognised, the detected bitness, and — importantly — any lines it failed to understand. If your Delphi version emits a shape I haven't anticipated, this is where it shows up. Send me those lines and I'll widen the parser.

Commands

units — the leaderboard

pascal
MapAnalyzer units MyApp.map --top=60
MapAnalyzer units MyApp.map --group=1            # roll up: Vcl.*, FireDAC.*, ...
MapAnalyzer units MyApp.map --exe=MyApp.exe      # what the map does NOT explain
MapAnalyzer units MyApp.map --all --csv=units.csv

--exe is worth running once. The gap between the map total and the real file size is resources — DFMs, {$R} data, icons, manifests — plus PE headers and padding. On form-heavy apps it is often larger than people expect, and no amount of uses-clause work will touch it.

routines — per-symbol sizes

pascal
MapAnalyzer routines MyApp.map --top=60
MapAnalyzer routines MyApp.map --generics        # generic instantiation bloat
MapAnalyzer routines MyApp.map --unit=System.SysUtils
MapAnalyzer routines MyApp.map --filter=TList

Sizes are the address gap to the next public symbol, clamped to the containing contribution. Private and local routines are not listed in the map, so their bytes fold into the public symbol above them: treat these as upper bounds, not exact measurements. They are still good enough to find the outliers.

--generics is where the surprises usually are — the same container specialised a dozen times, each paying full freight.

lines — bytes per source line

pascal
MapAnalyzer lines MyApp.map --unit=MyApp.BigUnit --top=40

Uses the map's line-number tables, so it needs a debug build. This is the one that connects back to the line count the IDE shows you: not "this unit is 400 KB" but "these 30 lines are 40 KB".

diff — the scoreboard

pascal
MapAnalyzer diff before.map after.map
MapAnalyzer diff before.map after.map --group=1

Build, change one thing, rebuild, diff. Turns "I think that helped" into "that removed 180 KB and dropped 23 units". For a long grind this is probably the command you'll run most.

uses — cost per uses-clause entry

pascal
MapAnalyzer uses MyApp.map ^
  --src=C:\Projects\MyApp ^
  --src="C:\Program Files (x86)\Embarcadero\Studio\23.0\source" ^
  --top=60 --csv=edges.csv

For every A uses B, reports how many bytes and how many units would actually leave the build if that one entry disappeared.

Point --src at the Delphi RTL/VCL source as well as your own. Without it the library units look like leaves, their dependencies are invisible, and every cost is understated. The tool warns you when linked modules have no readable source, and tells you how many.

An edge scoring 0 is reachable by another path too — cutting it alone frees nothing. That is usually most of them, and it is the point: it stops you spending a day on removals that change nothing.

Options: --root=<Unit> if the project file isn't in --src, --min=<bytes> to cut off the tail, --csv=<file>.

Global

--out=<file> writes the report to a file instead of the console.

How the uses numbers are computed

Each uses entry becomes its own node in the dependency graph (A → E → B), and the tool computes the dominator tree of that graph from the project root. The dominator subtree of E is exactly the set of units that become unreachable if that single edge is cut.

The edge-splitting matters. The obvious version — check whether A immediately dominates B — is wrong, and wrong in a way that silently inflates results: A dominating B means every path reaches node A, not that every path uses the edge A→B. In testing that formulation claimed 40 KB for an edge whose true cost was zero, because the target was still reachable another way.

The implementation is checked against brute force — delete the edge, recompute reachability, compare — over 60 randomised graphs containing cycles, shared paths, unreachable units, and units present in source but absent from the map.

Three things the numbers won't tell you

  1. Initialization side effects. A unit whose job is registration — Vcl.Imaging.Jpeg, FireDAC driver links, MidasLib, System.Win.ComObj, anything calling RegisterClass — will compile away cleanly and fail at run time. No static analysis catches this. Keep a whitelist.
  2. Interface vs implementation. The sect column matters. An intf entry propagates to everyone who uses that unit; an impl one doesn't. Moving a heavy intf entry down to the implementation section is usually cheaper and safer than removing it, and it's most of the compile-time win.
  3. Identifier collisions. Uses order decides which of two same-named symbols wins. Removing a unit can quietly change resolution rather than fail to compile.

Known limits

  • Resources aren't in the map at all (see --exe).
  • Contributions are attributed to the unit that emitted the code. For generics and inlined routines that's the instantiating unit, not the declaring one, which reads as misattribution if you don't expect it.
  • IFDEF branches are all treated as live when parsing uses clauses — deliberately conservative, so a conditional dependency is counted as real.
  • The graph only contains units the linker actually kept. Units dropped by the smart linker have no weight and no edges.