Timeline (TimeGraph) feature - #25
Conversation
…hArrow model - TimeGraphState now carries style, value, tags (types verified against the official TSP OpenAPI spec) - New TimeGraphArrow model (source_id, target_id, start, end, duration, style) - TimeGraph now carries an arrows list, populated separately via TimeGraph.parse_tsp_arrows since arrows come from a different TSP endpoint than states
- New 'timeline' subcommand: fetches TIME_GRAPH outputs via
experiment.find_outputs(type=['time_graph']) and TMLLClient.fetch_data
- Supports -k/--keywords, -o/--output, --start/--end, --entries
- Fetches arrows via fetch_timegraph_arrows and includes them in the
output as {states: [...], arrows: [...]}
- Adds -p/--plot to render a Gantt-style PNG
- fetch_timeline: wraps the CLI 'timeline' command, returns JSON with states and arrows for TIME_GRAPH outputs - plot_timeline: renders a Gantt-style PNG (reuses states_to_bars, group_states_by_entry, get_label_colors from the CLI drawing logic), returned as an MCP Image
kavehshahedi
left a comment
There was a problem hiding this comment.
Thanks for the contribution, Hiba! It looks very good to me!
Just a minor issue about code duplication, everything else is fine :)
| COLOR_PALETTE = ["green", "red", "blue", "orange", "purple", "gray"] | ||
| BAR_HEIGHT = 9 | ||
|
|
||
|
|
||
| def states_to_bars(states): | ||
| return [(state["start_time"], state["end_time"] - state["start_time"]) for state in states] | ||
|
|
||
|
|
||
| def group_states_by_entry(states): | ||
| grouped = {} | ||
| for state in states: | ||
| entry = state["entry_name"] | ||
| if entry not in grouped: | ||
| grouped[entry] = [] | ||
| grouped[entry].append(state) | ||
| return grouped | ||
|
|
||
|
|
||
| def get_label_colors(states): | ||
| label_colors = {} | ||
| for state in states: | ||
| label = state["label"] | ||
| if label is None or (isinstance(label, float) and label != label): | ||
| label = "(no label)" | ||
| if label not in label_colors: | ||
| label_colors[label] = COLOR_PALETTE[len( | ||
| label_colors) % len(COLOR_PALETTE)] | ||
| return label_colors | ||
|
|
||
|
|
||
| def draw_gantt_to_file(states, output_path): | ||
| import matplotlib | ||
| matplotlib.use("Agg") | ||
| import matplotlib.pyplot as plt | ||
| import matplotlib.patches as mpatches | ||
|
|
||
| label_colors = get_label_colors(states) | ||
| grouped = group_states_by_entry(states) | ||
|
|
||
| fig, ax = plt.subplots(figsize=(12, max(4, len(grouped) * 0.5))) | ||
| ax.set_title("TMLL Timeline — Gantt Chart") | ||
| ax.set_xlabel("Time (ns)") | ||
|
|
||
| yticks = [] | ||
| ylabels = [] | ||
| for i, (entry_name, entry_states) in enumerate(grouped.items()): | ||
| bars = states_to_bars(entry_states) | ||
| colors = [] | ||
| for state in entry_states: | ||
| label = state["label"] | ||
| if label is None or (isinstance(label, float) and label != label): | ||
| label = "(no label)" | ||
| colors.append(label_colors[label]) | ||
| y_pos = i * 10 | ||
| ax.broken_barh(bars, (y_pos, BAR_HEIGHT), facecolors=colors) | ||
| yticks.append(y_pos + BAR_HEIGHT / 2) | ||
| ylabels.append(entry_name) | ||
|
|
||
| ax.set_yticks(yticks) | ||
| ax.set_yticklabels(ylabels) | ||
| ax.set_ylim(-1, len(grouped) * 10) | ||
|
|
||
| handles = [mpatches.Patch(color=color, label=label) | ||
| for label, color in label_colors.items()] | ||
| ax.legend(handles=handles, loc="upper right") | ||
|
|
||
| fig.tight_layout() | ||
| plt.savefig(output_path, bbox_inches="tight") | ||
| plt.close(fig) |
There was a problem hiding this comment.
I feel that this code block has some duplications/redundancies with server.py. If their purpose is almost the same (or very similar), you can reuse them (or tweak a bit to be reusable).
There was a problem hiding this comment.
Good catch, thanks! I extracted the shared logic into tmll/mcp/gantt.py. Now both cli.py and server.py call build_gantt_figure() from there.
There was a problem hiding this comment.
Great! Thanks for refactoring the code.
Just one tiny minor suggestion: could you put gantt.py into a proper sub-directory to improve repo's structure? Maube inside tmll/mcp/plots/ or tmll/mcp/ui/? You decide which is more appropriate.
… Addresses review feedback from @kavehshahedi: states_to_bars, group_states_by_entry, get_label_colors, and the figure-building logic were duplicated between cli.py and server.py. Both now import build_gantt_figure() from the new shared module.
fetch_data's TIME_GRAPH case previously only extracted start_time/ end_time/label from each state, dropping style and value even though TimeGraphState parses them from the raw TSP response. Adds both fields to the DataFrame built in fetch_data. Fixes eclipse-tmll#26
What it does
Implements timeline (TimeGraph) support in TMLL (states, arrows, CLI, MCP tools):
TimeGraphStatenow carriesstyle,value,tags(types verified against the official TSP OpenAPI spec), in addition to existingstart/end/labelTimeGraphArrowmodel (source_id,target_id,start,end,duration,style);TimeGraphnow carries anarrowslist, populated separately viaTimeGraph.parse_tsp_arrowssince arrows come from a different TSP endpoint than statestimelinecommand: fetches TIME_GRAPH outputs viaexperiment.find_outputs(type=['time_graph'])andTMLLClient.fetch_data, with-k/--keywords,-o/--output,--start/--end,--entries, and-p/--plot(renders a Gantt-style PNG)fetch_timegraph_arrowsand included in the timeline output as{states: [...], arrows: [...]}fetch_timeline(wraps the CLI command) andplot_timeline(renders and returns a Gantt-style PNG as an MCP Image)How to test
Prerequisites:
cd tmll), with thetest-trace-filessubmodule initialized (git submodule update --init) and a localtrace-compass-serverrunning.--entriesto keep the image small/fast, omitting it renders every entry, which can be slow and hard to read for outputs with many rows):fetch_timelineandplot_timelinewere also tested via MCP Inspector:Follow-ups
TMLLClient.fetch_datadoesn't surfacestyle/valuefrom each state (labels are often empty for some TIME_GRAPH providers as a result) -- tracked in fetch_data doesn't surface TimeGraphState's style/value fields #26.fetch_data's defaultresample_freq(1s) means time windows narrower than ~1s return zero states, even if raw states exist in that window, worth keeping in mind when choosing--start/--end.-p/--plotonly renders the first output matching the given keywords if multiple outputs match.plot_timeline(similar toplot_xy_with_anomalies's Plotly-based interactive mode), for richer exploration than the static PNG.tmll/ml/modules/timeline/timeline_analysis_module.py), covering:cli.pyandserver.py(no logic changes, verified withgit diff -w). Happy to revert those specific formatting changes if a tighter diff is preferred.AI disclosure: Portions of this contribution (code and PR description) were created with the assistance of Claude Sonnet 5 (Anthropic). All AI-assisted code and output were reviewed and verified by me, including testing against a live trace-server, before submission.
Review checklist