Create a trace comparison tool to evaluate the performance impact of a change.
On many occasions, it's useful to understand the performance impact of a
change to GN. In particular, when investigating changes like making ninja
files per directory instead of per target, overall build times can be noisy
and hard to measure reliably.
`tools/compare_traces.py` compares Chrome trace logs from `gn gen --tracelog`
runs to isolate the performance impact on specific build files and phases.
By averaging multiple runs and filtering out noise, it can show when a change
reliably affects a specific part of the build even if the overall wall-clock
difference is small.
Example - I modified gn to sleep for 5 seconds while loading
//chrome/browser/BUILD.gn, then got the following results:
$ tools/compare_traces.py --before before_1.trace before_2.trace \
before_3.trace --after after_1.trace after_2.trace after_3.trace
======================================================================================
GN PERFORMANCE IMPACT REPORT
======================================================================================
Samples: Before: 3 run(s) | After: 3 run(s)
EXECUTIVE VERDICT
--------------------------------------------------------------------------------------
⚠️ REGRESSION DETECTED: Slower by +2,420 ms (+67.6%)
• Wall-clock / run: Before: 3,577 ± 365 ms -> After: 5,997 ± 17 ms
• Total CPU / run: Before: 64,738 ± 4,101 ms -> After: 76,680 ± 1,848 ms
• Events / run: Before: 152,059 -> After: 152,059 (Δ +0)
AFFECTED GN PHASES (Ranked by impact)
--------------------------------------------------------------------------------------
Delta (ms) Delta (%) Before After Name
--------------------------------------------------------------------------------------
+6,167 ms +31.5% 19,584 ms 25,752 ms BUILD.gn Execution
+26 ms +4.1% 634 ms 660 ms External Scripts
AFFECTED SUBSYSTEMS (Ranked by impact)
--------------------------------------------------------------------------------------
Delta (ms) Delta (%) Before After Name
--------------------------------------------------------------------------------------
-22 ms -65.8% 33 ms 11 ms //remoting/base
AFFECTED BUILD FILES (Ranked by impact)
--------------------------------------------------------------------------------------
Delta (ms) Delta (%) Before After Name
--------------------------------------------------------------------------------------
+4,940 ms +7093.8% 70 ms 5,009 ms //chrome/browser/BUILD.gn
+25 ms +19.8% 124 ms 149 ms //chrome/test/BUILD.gn
+24 ms +84.1% 28 ms 52 ms //components/omnibox/browser/BUILD.gn
-22 ms -68.5% 32 ms 10 ms //remoting/base/BUILD.gn
-21 ms -69.8% 30 ms 9 ms //ui/gl/mojom/BUILD.gn
-20 ms -66.8% 30 ms 10 ms //remoting/protocol/BUILD.gn
+10 ms +31.7% 30 ms 40 ms //extensions/browser/BUILD.gn
-12 ms -37.8% 32 ms 20 ms //ui/accessibility/mojom/BUILD.gn
Change-Id: Ie31079d5836559d850d61692021b1e346a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/25680
Commit-Queue: Matt Stark <msta@google.com>
Reviewed-by: Takuto Ikuta <tikuta@google.com>
diff --git a/tools/compare_traces.py b/tools/compare_traces.py
new file mode 100755
index 0000000..08de08c
--- /dev/null
+++ b/tools/compare_traces.py
@@ -0,0 +1,594 @@
+#!/usr/bin/env python3
+# Copyright 2026 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""compare_traces.py - Performance Impact Analysis & Trace Differ for GN.
+
+Loads traces from two sets of `gn gen --tracelog=...` invocations, with each
+set potentially containing multiple different tracelog files, and attempts to
+aggregate, evaluate, and render the difference in performance between them in a
+human-readable format.
+"""
+
+import argparse
+import collections
+import dataclasses
+import json
+import math
+import pathlib
+import statistics
+from typing import Dict, List, Optional, Tuple
+
+DEFAULT_MIN_DELTA_MS = 5.0
+DEFAULT_ALPHA = 0.05
+REGRESSION_WEIGHT = 1.5
+SUBSYSTEMS_TABLE_LIMIT = 10
+BUILD_FILE_TABLE_LIMIT = 15
+
+CATEGORY_DESCRIPTIONS = {
+ 'file_write_ninja': 'Ninja File Emission',
+ 'file_exec_template': 'Template Invocations',
+ 'file_exec': 'BUILD.gn Execution',
+ 'define': 'Target Definitions',
+ 'onresolved': 'Target Resolution',
+ 'import_load': 'Import Loading',
+ 'script_exec': 'External Scripts',
+ 'parse': 'AST Parsing',
+ 'load': 'File Loading',
+ 'import_block': 'Import Block',
+ 'file_write_generated': 'Generated File Emission',
+ 'walk_metadata': 'Metadata Walks',
+ 'setup': 'Initialization & Setup',
+}
+
+
+class Stat:
+ """Maintains sample statistics (mean, sample stddev, variance) for a metric."""
+
+ def __init__(self, values: List[float]):
+ self.values = values
+ self.n = len(values)
+ self.mean = statistics.mean(values) if values else 0.0
+ self.variance = statistics.variance(values) if self.n > 1 else 0.0
+ self.stddev = statistics.stdev(values) if self.n > 1 else 0.0
+
+ def format_int(self, show_stddev: bool = True) -> str:
+ """Formats mean and standard deviation rounded to integer milliseconds."""
+ if self.n > 1 and show_stddev and round(self.stddev) > 0:
+ return f'{round(self.mean):,d} ± {round(self.stddev):,d}'
+ return f'{round(self.mean):,d}'
+
+
+@dataclasses.dataclass
+class SingleTraceMetrics:
+ """Extracted metrics from a single Chrome trace JSON run."""
+
+ wall_clock_ms: float
+ total_cpu_ms: float
+ total_events: int
+ categories: Dict[str, dict]
+ subsystems: Dict[str, dict]
+ file_execs: Dict[str, dict]
+ script_execs: Dict[str, dict]
+
+
+@dataclasses.dataclass
+class AggregatedTrace:
+ """Aggregated statistics across multiple trace runs for a single revision."""
+
+ run_count: int
+ wall_clock: Stat
+ cpu_time: Stat
+ events: Stat
+ categories: Dict[str, Dict[str, Stat]]
+ subsystems: Dict[str, Dict[str, Stat]]
+ file_execs: Dict[str, Dict[str, Stat]]
+ script_execs: Dict[str, Dict[str, Stat]]
+
+
+@dataclasses.dataclass
+class ChangedItem:
+ name: str
+ before_ms: float
+ after_ms: float
+ delta_ms: float
+ delta_pct: float
+ p_val: Optional[float]
+ relevance_score: float
+
+
+def _betacf(a: float, b: float, x: float, max_iter: int = 200) -> float:
+ """Continued fraction for regularized incomplete beta function (Lentz method)."""
+ qab = a + b
+ qap = a + 1.0
+ qam = a - 1.0
+ c, d = 1.0, 1.0 - qab * x / qap
+ if abs(d) < 1e-30:
+ d = 1e-30
+ d = 1.0 / d
+ h = d
+ for m in range(1, max_iter):
+ m2 = 2 * m
+ aa = m * (b - m) * x / ((qam + m2) * (a + m2))
+ d = 1.0 + aa * d
+ if abs(d) < 1e-30:
+ d = 1e-30
+ c = 1.0 + aa / c
+ if abs(c) < 1e-30:
+ c = 1e-30
+ d = 1.0 / d
+ h *= d * c
+ aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2))
+ d = 1.0 + aa * d
+ if abs(d) < 1e-30:
+ d = 1e-30
+ c = 1.0 + aa / c
+ if abs(c) < 1e-30:
+ c = 1e-30
+ d = 1.0 / d
+ del_h = d * c
+ h *= del_h
+ if abs(del_h - 1.0) < 1e-12:
+ break
+ return h
+
+
+def betainc(a: float, b: float, x: float) -> float:
+ """Regularized incomplete beta function I_x(a, b)."""
+ if x <= 0:
+ return 0.0
+ if x >= 1:
+ return 1.0
+ front = math.exp(
+ math.lgamma(a + b)
+ - math.lgamma(a)
+ - math.lgamma(b)
+ + a * math.log(x)
+ + b * math.log(1.0 - x)
+ )
+ if x < (a + 1.0) / (a + b + 2.0):
+ return front * _betacf(a, b, x) / a
+ else:
+ return 1.0 - front * _betacf(b, a, 1.0 - x) / b
+
+
+def compute_p_value(b_stat: Stat, a_stat: Stat) -> Optional[float]:
+ """Computes two-tailed p-value using Welch's t-test with Welch-Satterthwaite df."""
+ if b_stat.n < 2 or a_stat.n < 2:
+ return None
+
+ v1 = b_stat.variance / b_stat.n
+ v2 = a_stat.variance / a_stat.n
+ denom = math.sqrt(v1 + v2)
+ if denom == 0:
+ return 1.0 if b_stat.mean == a_stat.mean else 0.0
+
+ t = (a_stat.mean - b_stat.mean) / denom
+ df_num = (v1 + v2) ** 2
+ df_den = (v1**2) / (b_stat.n - 1) + (v2**2) / (a_stat.n - 1)
+ df = df_num / df_den if df_den > 0 else 1.0
+
+ x = df / (df + t * t)
+ return betainc(0.5 * df, 0.5, x)
+
+
+def benjamini_hochberg_adjust(p_values: List[float]) -> List[float]:
+ """Controls False Discovery Rate (FDR) across thousands of parallel comparisons.
+
+ When evaluating thousands of files, a standard alpha=0.05 cutoff produces ~5%
+ false positives by pure chance. The Benjamini-Hochberg procedure adjusts
+ p-values so the global expected false discovery rate remains below alpha.
+ """
+ m = len(p_values)
+ if m == 0:
+ return []
+
+ indexed_p = sorted(enumerate(p_values), key=lambda x: x[1])
+ adjusted = [0.0] * m
+
+ running_min = 1.0
+ for rank_rev, (orig_idx, p) in enumerate(reversed(indexed_p)):
+ rank = m - rank_rev
+ adj_p = min(1.0, (m / rank) * p)
+ running_min = min(running_min, adj_p)
+ adjusted[orig_idx] = running_min
+
+ return adjusted
+
+
+def compute_relevance_score(delta_ms: float, p_val: Optional[float]) -> float:
+ """Ranks changes by actionability: magnitude * certainty * regression priority.
+
+ Regressions receive a 1.5x weight over speedups so problematic files appear
+ at the top of the report.
+ """
+ abs_delta = abs(delta_ms)
+ direction_weight = REGRESSION_WEIGHT if delta_ms > 0 else 1.0
+
+ if p_val is not None:
+ # Scale certainty by -log10(p): p=0.001 -> 3.0, p=0.01 -> 2.0, p=0.05 -> 1.3
+ certainty = -math.log10(max(p_val, 1e-4))
+ else:
+ certainty = 1.0
+
+ return abs_delta * certainty * direction_weight
+
+
+def load_trace_events(trace_path: pathlib.Path) -> List[dict]:
+ """Reads Chrome trace JSON file and returns the list of traceEvents."""
+ with open(trace_path, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+ return data.get('traceEvents', [])
+
+
+def _get_subsystem(name: str) -> str:
+ """Extracts the top-level subsystem directory (e.g. //foo/bar)."""
+ package = name.lstrip('/').split('/')[:-1]
+ # Eg. //foo/bar/baz/BUILD.gn -> //foo/bar
+ return '//' + '/'.join(package[:2])
+
+
+def extract_single_trace_metrics(events: List[dict]) -> SingleTraceMetrics:
+ """Processes raw events for one run into categories, subsystems, and files."""
+ no_event = lambda: {'dur_us': 0, 'count': 0}
+ categories = collections.defaultdict(no_event)
+ by_category = collections.defaultdict(
+ lambda: collections.defaultdict(no_event)
+ )
+ subsystems = collections.defaultdict(no_event)
+ by_thread_intervals = collections.defaultdict(list)
+
+ def add_event(collection: dict, key: str, dur_us: int):
+ entry = collection[key]
+ entry['dur_us'] += dur_us
+ entry['count'] += 1
+
+ min_ts = None
+ max_ts = None
+ total_events = 0
+
+ for event in events:
+ # Only complete events ('ph': 'X') represent measured function execution
+ if event.get('ph') != 'X':
+ continue
+
+ total_events += 1
+ cat = event.get('cat', 'unknown')
+ dur = event.get('dur', 0)
+ ts = event.get('ts', 0)
+ name = event.get('name', '')
+ tid = event.get('tid', 0)
+
+ min_ts = ts if min_ts is None else min(min_ts, ts)
+ max_ts = ts + dur if max_ts is None else max(max_ts, ts + dur)
+
+ add_event(categories, cat, dur)
+ add_event(by_category[cat], name, dur)
+ by_thread_intervals[tid].append((ts, ts + dur))
+
+ if cat in ('file_exec', 'parse'):
+ add_event(subsystems, _get_subsystem(name), dur)
+
+ wall_clock_ms = (
+ ((max_ts - min_ts) / 1000.0)
+ if (min_ts is not None and max_ts is not None)
+ else 0.0
+ )
+
+ # Merge overlapping intervals per thread to eliminate nested event overcounting
+ total_cpu_us = 0
+ for intervals in by_thread_intervals.values():
+ intervals.sort()
+ last = float('-inf')
+ for start, end in intervals:
+ start = max(start, last)
+ total_cpu_us += max(end - start, 0)
+ last = max(last, end)
+
+ return SingleTraceMetrics(
+ wall_clock_ms=wall_clock_ms,
+ total_cpu_ms=total_cpu_us / 1000,
+ total_events=total_events,
+ categories=categories,
+ subsystems=subsystems,
+ file_execs=by_category['file_exec'],
+ script_execs=by_category['script_exec'],
+ )
+
+
+def _aggregate_metric_map(
+ runs_metrics: List[Dict[str, dict]],
+) -> Dict[str, Dict[str, Stat]]:
+ """Aggregates a metric map (categories, files, etc.) across multiple runs into Stats."""
+ all_keys = set()
+ for m in runs_metrics:
+ all_keys.update(m.keys())
+
+ aggregated = {}
+ for key in all_keys:
+ durs = [m.get(key, {}).get('dur_us', 0) / 1000.0 for m in runs_metrics]
+ counts = [m.get(key, {}).get('count', 0) for m in runs_metrics]
+ aggregated[key] = {
+ 'duration_ms': Stat(durs),
+ 'count': Stat(counts),
+ }
+ return aggregated
+
+
+def aggregate_multiple_traces(
+ trace_paths: List[pathlib.Path],
+) -> AggregatedTrace:
+ """Loads multiple trace files for a revision and returns averaged Stats."""
+ runs = [
+ extract_single_trace_metrics(load_trace_events(p)) for p in trace_paths
+ ]
+
+ return AggregatedTrace(
+ run_count=len(runs),
+ wall_clock=Stat([r.wall_clock_ms for r in runs]),
+ cpu_time=Stat([r.total_cpu_ms for r in runs]),
+ events=Stat([r.total_events for r in runs]),
+ categories=_aggregate_metric_map([r.categories for r in runs]),
+ subsystems=_aggregate_metric_map([r.subsystems for r in runs]),
+ file_execs=_aggregate_metric_map([r.file_execs for r in runs]),
+ script_execs=_aggregate_metric_map([r.script_execs for r in runs]),
+ )
+
+
+def find_significant_changes(
+ b_map: Dict[str, Dict[str, Stat]],
+ a_map: Dict[str, Dict[str, Stat]],
+ b_count: int,
+ a_count: int,
+ min_delta_ms: float,
+ alpha: float,
+ has_multi_run: bool,
+) -> List[ChangedItem]:
+ """Identifies items with significant deltas, applies FDR correction, and ranks by relevance."""
+ all_keys = set(b_map.keys()) | set(a_map.keys())
+ candidates = []
+
+ for key in all_keys:
+ b_stat = b_map.get(key, {}).get('duration_ms', Stat([0.0] * b_count))
+ a_stat = a_map.get(key, {}).get('duration_ms', Stat([0.0] * a_count))
+ d_ms = a_stat.mean - b_stat.mean
+
+ if abs(d_ms) >= min_delta_ms:
+ p_val = compute_p_value(b_stat, a_stat) if has_multi_run else None
+ candidates.append((key, b_stat.mean, a_stat.mean, d_ms, p_val))
+
+ if has_multi_run:
+ testable = [c for c in candidates if c[4] is not None]
+ adj_p_values = benjamini_hochberg_adjust([c[4] for c in testable])
+ significant_items = [
+ ChangedItem(
+ key,
+ b_ms,
+ a_ms,
+ d_ms,
+ (d_ms / b_ms * 100.0) if b_ms > 0 else 0.0,
+ adj_p,
+ compute_relevance_score(d_ms, adj_p),
+ )
+ for (key, b_ms, a_ms, d_ms, _), adj_p in zip(testable, adj_p_values)
+ if adj_p < alpha
+ ]
+ else:
+ significant_items = [
+ ChangedItem(
+ key,
+ b_ms,
+ a_ms,
+ d_ms,
+ (d_ms / b_ms * 100.0) if b_ms > 0 else 0.0,
+ None,
+ compute_relevance_score(d_ms, None),
+ )
+ for key, b_ms, a_ms, d_ms, _ in candidates
+ ]
+
+ # Rank by Relevance Score (regressions first, then largest speedups)
+ significant_items.sort(key=lambda item: item.relevance_score, reverse=True)
+ return significant_items
+
+
+def print_executive_verdict(
+ b: AggregatedTrace, a: AggregatedTrace, alpha: float
+) -> None:
+ """Prints high-level conclusion on whether performance changed."""
+ wall_delta = a.wall_clock.mean - b.wall_clock.mean
+ wall_pct = (
+ (wall_delta / b.wall_clock.mean * 100.0) if b.wall_clock.mean > 0 else 0.0
+ )
+ wall_p = compute_p_value(b.wall_clock, a.wall_clock)
+ has_multi_run = b.run_count > 1 and a.run_count > 1
+
+ print('\nEXECUTIVE VERDICT')
+ print('-' * 86)
+ if not has_multi_run:
+ sign = '+' if round(wall_delta) > 0 else ''
+ print(
+ ' Single-run comparison: Wall-clock delta'
+ f' {sign}{round(wall_delta):,d} ms ({sign}{wall_pct:.1f}%)'
+ )
+ print(
+ ' (Tip: Pass multiple --before and --after runs for statistical'
+ ' confidence testing)'
+ )
+ elif wall_p is not None and wall_p < alpha and abs(wall_pct) >= 1.0:
+ if wall_delta > 0:
+ print(
+ f' ⚠️ REGRESSION DETECTED: Slower by +{round(wall_delta):,d} ms'
+ f' (+{wall_pct:.1f}%)'
+ )
+ else:
+ print(
+ f' 🚀 SPEEDUP DETECTED: Faster by -{abs(round(wall_delta)):,d} ms'
+ f' ({wall_pct:.1f}%)'
+ )
+ else:
+ print(
+ ' ✓ NO MEASURABLE OVERALL IMPACT DETECTED (Wall-clock delta'
+ f' {round(wall_delta):+d} ms / {wall_pct:+.1f}%)'
+ )
+
+ b_wc = f'{b.wall_clock.format_int()} ms'
+ a_wc = f'{a.wall_clock.format_int()} ms'
+ b_cpu = f'{b.cpu_time.format_int()} ms'
+ a_cpu = f'{a.cpu_time.format_int()} ms'
+ b_ev = f'{round(b.events.mean):,d}'
+ a_ev = f'{round(a.events.mean):,d}'
+ ev_delta = f'(Δ {round(a.events.mean - b.events.mean):+d})'
+
+ print(f'\n • Wall-clock / run: Before: {b_wc:>17} -> After: {a_wc:>17}')
+ print(f' • Total CPU / run: Before: {b_cpu:>17} -> After: {a_cpu:>17}')
+ print(
+ f' • Events / run: Before: {b_ev:>17} -> After: {a_ev:>17} '
+ f' {ev_delta}'
+ )
+ print()
+
+
+def print_changed_items_table(
+ item_type: str,
+ items: List[ChangedItem],
+ max_items: int,
+) -> None:
+ """Prints a ranked table of changed items, or a clean confirmation if empty."""
+ print(f'AFFECTED {item_type.upper()} (Ranked by impact)')
+ print('-' * 86)
+ if items:
+ print(
+ f'{"Delta (ms)":>11} {"Delta (%)":>10} {"Before":>11} {"After":>11}'
+ f' {"Name"}'
+ )
+ print('-' * 86)
+ for item in items[:max_items]:
+ sign = '+' if round(item.delta_ms) > 0 else ''
+ pct_sign = '+' if item.delta_pct > 0 else ''
+ print(
+ f'{sign + f"{round(item.delta_ms):,d} ms":>11}'
+ f' {pct_sign + f"{item.delta_pct:.1f}%":>9}'
+ f' {round(item.before_ms):>8,d} ms {round(item.after_ms):>8,d} ms '
+ f' {item.name}'
+ )
+ else:
+ print(
+ f' ✓ Discarded all noise: No {item_type} had statistically significant'
+ ' regressions or speedups.'
+ )
+ print()
+
+
+def analyze_and_report(
+ b: AggregatedTrace,
+ a: AggregatedTrace,
+ min_delta_ms: float = DEFAULT_MIN_DELTA_MS,
+ alpha: float = DEFAULT_ALPHA,
+) -> None:
+ """Main coordinator: evaluates traces and produces the final cleaned report."""
+ has_multi_run = b.run_count > 1 and a.run_count > 1
+
+ print('=' * 86)
+ print(' GN PERFORMANCE IMPACT REPORT')
+ print('=' * 86)
+ print(f'Samples: Before: {b.run_count} run(s) | After: {a.run_count} run(s)')
+
+ print_executive_verdict(b, a, alpha)
+
+ phase_changes = find_significant_changes(
+ b.categories,
+ a.categories,
+ b.run_count,
+ a.run_count,
+ min_delta_ms,
+ alpha,
+ has_multi_run,
+ )
+ for item in phase_changes:
+ item.name = CATEGORY_DESCRIPTIONS.get(item.name, item.name)
+ print_changed_items_table(
+ 'GN phases', phase_changes, max_items=len(CATEGORY_DESCRIPTIONS)
+ )
+
+ subsystem_changes = find_significant_changes(
+ b.subsystems,
+ a.subsystems,
+ b.run_count,
+ a.run_count,
+ min_delta_ms,
+ alpha,
+ has_multi_run,
+ )
+ print_changed_items_table(
+ 'subsystems', subsystem_changes, max_items=SUBSYSTEMS_TABLE_LIMIT
+ )
+
+ file_changes = find_significant_changes(
+ b.file_execs,
+ a.file_execs,
+ b.run_count,
+ a.run_count,
+ min_delta_ms,
+ alpha,
+ has_multi_run,
+ )
+ print_changed_items_table(
+ 'BUILD files', file_changes, max_items=BUILD_FILE_TABLE_LIMIT
+ )
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(
+ description=(
+ 'GN Trace Impact Report: Discards noise, ranks by relevance, and'
+ ' highlights affected parts.'
+ ),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""Examples:
+ compare_traces.py --before 1.trace 2.trace 3.trace --after 4.trace 5.trace 6.trace
+ compare_traces.py --before before.trace --after after.trace
+""",
+ )
+ parser.add_argument(
+ '--before',
+ nargs='+',
+ action='extend',
+ type=pathlib.Path,
+ required=True,
+ help='One or more "before" trace JSON files.',
+ )
+ parser.add_argument(
+ '--after',
+ nargs='+',
+ action='extend',
+ type=pathlib.Path,
+ required=True,
+ help='One or more "after" trace JSON files.',
+ )
+ parser.add_argument(
+ '--min-delta',
+ type=float,
+ default=DEFAULT_MIN_DELTA_MS,
+ help=(
+ 'Minimum effect size in ms to consider (default:'
+ f' {DEFAULT_MIN_DELTA_MS}).'
+ ),
+ )
+ parser.add_argument(
+ '--alpha',
+ type=float,
+ default=DEFAULT_ALPHA,
+ help=f'Significance threshold FDR Q-value (default: {DEFAULT_ALPHA}).',
+ )
+ args = parser.parse_args()
+
+ before = aggregate_multiple_traces(args.before)
+ after = aggregate_multiple_traces(args.after)
+ analyze_and_report(
+ before, after, min_delta_ms=args.min_delta, alpha=args.alpha
+ )
+
+
+if __name__ == '__main__':
+ main()