#!/usr/bin/env python3 """ dp_frag.py -- C28x data-page fragmentation analyzer ===================================================== Usage: ofd2000 -t yourfile.out > symbols.txt python dp_frag.py symbols.txt [--section .ebss] [--html report.html] Outputs: - Console summary: waste %, worst pages, suggested reordering - Optional standalone HTML report (same visual as the CCS E2E widget) C28x DP page = 64 words (0x40). Any gap between the end of one symbol and the start of the next within the same page is wasted RAM because the linker cannot overlap it with another object after the fact. """ import re import sys import argparse from collections import defaultdict from pathlib import Path DP_SIZE = 64 # words per data page on C28x # --------------------------------------------------------------------------- # Parsing # --------------------------------------------------------------------------- def parse_ofd2000(text: str, section_filter: str = None): """ Parse ofd2000 -t output. Expected line formats (the tool emits several): SYMBOL TABLE: [ 0] (sec 3)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000002 motorSpeed ... OR the simpler flat-dump form: motorSpeed 0x00000002 1 .ebss We accept both. Returns list of dicts: {name, addr, size, section}. """ symbols = [] # Pattern 1: COFF symbol table dump from ofd2000 -t # Lines look like: [ N] (sec S)(fl ...)(ty ...)(scl ...) (nx 0) 0xADDR NAME # Size is on the *next* line if nx>0, or we pick it from the aux record. # Simpler: use the XML mode instead (ofd2000 -g --xml_indent=0) — but # we also handle the flat text. # Pattern 2: line with NAME HEX_ADDR DECIMAL_SIZE [SECTION] flat_re = re.compile( r'^\s*(\w+)\s+' # symbol name r'(?:0x)?([0-9A-Fa-f]+)\s+' # address (hex, optional 0x) r'(\d+)' # size in words (decimal) r'(?:\s+(\S+))?', # optional section name re.MULTILINE ) # Pattern 3: .map file "GLOBAL DATA SYMBOLS" section # _motorSpeed 0000 0002 .ebss map_re = re.compile( r'^\s*(\w+)\s+([0-9A-Fa-f]{4,8})\s+([0-9A-Fa-f]{4,8})\s+(\S+)', re.MULTILINE ) found = set() for m in flat_re.finditer(text): name, addr_hex, size_str, sec = m.group(1), m.group(2), m.group(3), m.group(4) # Skip obvious non-symbols if name.lower() in ('section', 'name', 'symbol', 'address', 'size', 'file'): continue addr = int(addr_hex, 16) size = int(size_str) if size == 0: continue if section_filter and sec and section_filter not in sec: continue key = (name, addr) if key not in found: found.add(key) symbols.append({'name': name, 'addr': addr, 'size': size, 'section': sec or 'unknown'}) if not symbols: # Try map-file format: address is col2, size is col3 for m in map_re.finditer(text): name, addr_hex, size_hex, sec = m.groups() if name.lower().startswith(('section', 'name', 'output')): continue addr = int(addr_hex, 16) size = int(size_hex, 16) if size == 0: continue if section_filter and section_filter not in sec: continue key = (name, addr) if key not in found: found.add(key) symbols.append({'name': name, 'addr': addr, 'size': size, 'section': sec}) return symbols # --------------------------------------------------------------------------- # Analysis # --------------------------------------------------------------------------- def assign_pages(symbols): """Group symbols by DP page index.""" pages = defaultdict(list) for s in symbols: pg = s['addr'] // DP_SIZE pages[pg].append(s) return dict(sorted(pages.items())) def analyze_page(page_idx, syms): """ Return per-page analysis dict. gaps = wasted words due to alignment holes between symbols on this page. tail = unused words after the last symbol before the page boundary. """ base = page_idx * DP_SIZE sorted_syms = sorted(syms, key=lambda s: s['addr']) cursor = base gaps = [] # list of (start_addr, length) tuples used = 0 for s in sorted_syms: if s['addr'] > cursor: gaps.append((cursor, s['addr'] - cursor)) # Sanity: symbol overflows page boundary end = s['addr'] + s['size'] used += s['size'] cursor = end page_end = base + DP_SIZE tail = max(0, page_end - cursor) return { 'page': page_idx, 'base': base, 'syms': sorted_syms, 'used': used, 'gaps': gaps, 'gap_words': sum(g[1] for g in gaps), 'tail': tail, } def global_stats(page_analyses): total_pages = len(page_analyses) total_capacity = total_pages * DP_SIZE total_used = sum(p['used'] for p in page_analyses) total_gap = sum(p['gap_words'] for p in page_analyses) total_tail = sum(p['tail'] for p in page_analyses) waste_pct = total_gap / total_capacity * 100 if total_capacity else 0 return { 'pages': total_pages, 'capacity': total_capacity, 'used': total_used, 'gap': total_gap, 'tail': total_tail, 'waste_pct': waste_pct, } # --------------------------------------------------------------------------- # Suggestions # --------------------------------------------------------------------------- def suggest_reorder(page_analyses): """ For pages with gaps, suggest a reordering: put large symbols first (they align to the page start naturally) then pack scalars. Returns list of (page_idx, suggested_order_names, estimated_savings). """ suggestions = [] for pa in page_analyses: if pa['gap_words'] == 0: continue syms = pa['syms'] # Sort by size descending reordered = sorted(syms, key=lambda s: s['size'], reverse=True) # Simulate packing simulated_gap = 0 cursor = pa['base'] for s in reordered: # Align to natural power-of-2 boundary (max 8 words for arrays/structs) align = min(8, _next_pow2(s['size'])) aligned_start = ((cursor + align - 1) // align) * align simulated_gap += aligned_start - cursor cursor = aligned_start + s['size'] savings = pa['gap_words'] - simulated_gap if savings > 0: suggestions.append({ 'page': pa['page'], 'current_gap': pa['gap_words'], 'simulated_gap': simulated_gap, 'savings': savings, 'order': [s['name'] for s in reordered], }) return sorted(suggestions, key=lambda x: x['savings'], reverse=True) def _next_pow2(n): if n <= 1: return 1 p = 1 while p < n: p <<= 1 return p # --------------------------------------------------------------------------- # Console report # --------------------------------------------------------------------------- def print_report(page_analyses, stats, suggestions): W = 72 print('=' * W) print(' C28x DP Fragmentation Report') print('=' * W) print(f" Data pages occupied : {stats['pages']}") print(f" Total capacity : {stats['capacity']} words ({stats['capacity']*2} bytes)") print(f" Used by symbols : {stats['used']} words") print(f" Alignment gaps : {stats['gap']} words ({stats['waste_pct']:.1f}% of capacity)") print(f" Free tail space : {stats['tail']} words") print() if stats['gap'] == 0: print(' No alignment gaps found — packing looks optimal.') return print(' Per-page breakdown (pages with gaps only):') print(' ' + '-' * 68) print(f" {'DP':>4} {'base':>8} {'used':>5} {'gaps':>5} {'tail':>5} map") print(' ' + '-' * 68) for pa in page_analyses: if pa['gap_words'] == 0 and pa['tail'] > 16: continue bar = _ascii_bar(pa['syms'], pa['base'], width=24) print(f" DP{pa['page']:<3} 0x{pa['base']:04X} {pa['used']:>4}w {pa['gap_words']:>4}w {pa['tail']:>4}w {bar}") for s in pa['syms']: print(f" 0x{s['addr']:04X} {s['name']:<24} {s['size']:>3}w") if pa['gaps']: for gaddr, glen in pa['gaps']: print(f" 0x{gaddr:04X} {'*** gap ***':<24} {glen:>3}w <-- wasted") print() if suggestions: print(' Reorder suggestions (by estimated word savings):') print(' ' + '-' * 68) for sg in suggestions[:5]: print(f" DP{sg['page']}: current gap={sg['current_gap']}w " f"after reorder≈{sg['simulated_gap']}w saves≈{sg['savings']}w") print(f" suggested order: {', '.join(sg['order'])}") print() print(' Note: reorder suggestions assume natural power-of-2 alignment.') print(' Verify with your linker CMD file constraints.') print('=' * W) def _ascii_bar(syms, base, width=24): bar = ['.'] * width for s in syms: start = s['addr'] - base for i in range(s['size']): pos = int((start + i) / DP_SIZE * width) if 0 <= pos < width: bar[pos] = '#' return ''.join(bar) # --------------------------------------------------------------------------- # HTML report # --------------------------------------------------------------------------- HTML_TEMPLATE = r""" C28x DP Fragmentation Report

C28x DP Fragmentation Report

alignment waste
WASTE_PCT%
gap words
GAP_W w
used words
USED_W w
data pages
N_PAGES
used alignment gap free
PAGE_BARS
click a page to inspect symbols
""" def build_html(page_analyses, stats, suggestions): bars_html = '' page_data_js = '[' for pa in page_analyses: base = pa['base'] syms_sorted = sorted(pa['syms'], key=lambda s: s['addr']) cursor = base segs = '' for s in syms_sorted: if s['addr'] > cursor: w = s['addr'] - cursor segs += f'
' segs += f'
' cursor = s['addr'] + s['size'] tail = base + DP_SIZE - cursor if tail > 0: segs += f'
' waste_pct = round(pa['gap_words'] / DP_SIZE * 100) color = '#BA7517' if waste_pct > 20 else '#888' idx = len(page_analyses) - page_analyses.index(pa) - 1 # for click handler idx = page_analyses.index(pa) bars_html += ( f'
' f'
DP{pa["page"]}
' f'
{segs}
' f'
{waste_pct}%
' f'
\n' ) syms_js = '[' + ','.join( f'{{"name":"{s["name"]}","addr":{s["addr"]},"size":{s["size"]}}}' for s in pa['syms'] ) + ']' page_data_js += ( f'{{"page":{pa["page"]},"used":{pa["used"]},' f'"gap_words":{pa["gap_words"]},"tail":{pa["tail"]},' f'"syms":{syms_js}}},' ) page_data_js = page_data_js.rstrip(',') + ']' html = HTML_TEMPLATE html = html.replace('WASTE_PCT', f"{stats['waste_pct']:.1f}") html = html.replace('GAP_W', str(stats['gap'])) html = html.replace('USED_W', str(stats['used'])) html = html.replace('N_PAGES', str(stats['pages'])) html = html.replace('PAGE_BARS', bars_html) html = html.replace('PAGE_DATA', page_data_js) return html # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main(): parser = argparse.ArgumentParser( description='C28x DP fragmentation analyzer. ' 'Pipe ofd2000 -t yourfile.out into this script or pass as file.') parser.add_argument('input', nargs='?', default='-', help='ofd2000 -t output file (default: stdin)') parser.add_argument('--section', default='.ebss', help='filter to this section (default: .ebss)') parser.add_argument('--html', metavar='FILE', help='write standalone HTML report to FILE') parser.add_argument('--all-sections', action='store_true', help='ignore section filter, analyze all symbols') args = parser.parse_args() if args.input == '-': text = sys.stdin.read() else: text = Path(args.input).read_text(errors='replace') section = None if args.all_sections else args.section symbols = parse_ofd2000(text, section_filter=section) if not symbols: print(f'ERROR: no symbols found. Check input format or try --all-sections.') print(' Expected: ofd2000 -t yourfile.out | python dp_frag.py') sys.exit(1) print(f'Parsed {len(symbols)} symbols', end='') if section: print(f' in section {section}', end='') print() pages = assign_pages(symbols) page_analyses = [analyze_page(idx, syms) for idx, syms in pages.items()] stats = global_stats(page_analyses) suggestions = suggest_reorder(page_analyses) print_report(page_analyses, stats, suggestions) if args.html: html = build_html(page_analyses, stats, suggestions) Path(args.html).write_text(html) print(f'HTML report written to {args.html}') if __name__ == '__main__': main()