OXYGENHUB
◆ main openmsx / src / debugger / Tracer.cc
15.2 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "Tracer.hh"
2
3
#include "Debugger.hh"
4
#include "ProbeBreakPoint.hh"
5
6
#include "Interpreter.hh"
7
#include "MSXMotherBoard.hh"
8
#include "ReverseManager.hh"
9
#include "StateChangeDistributor.hh"
10
#include "TclArgParser.hh"
11
12
#include "Date.hh"
13
#include "one_of.hh"
14
#include "stl.hh"
15
#include "strCat.hh"
16
17
#include <cassert>
18
#include <format>
19
#include <queue>
20
21
namespace openmsx {
22
23
Tracer::Tracer(Debugger& debugger)
24
	: stateChangeDistributor(debugger.getMotherBoard().getStateChangeDistributor())
25
{
26
	stateChangeDistributor.registerListener(*this);
27
}
28
29
Tracer::~Tracer()
30
{
31
	stateChangeDistributor.unregisterListener(*this);
32
}
33
34
void Tracer::Trace::addEvent(EmuTime t, TraceValue v, bool merge)
35
{
36
	assert(events.empty() || t >= events.back().time);
37
38
	if (merge && !events.empty() && (events.back().value == v)) {
39
		return;
40
	}
41
42
	using enum Type;
43
	auto valueFormat = v.visit(overloaded{
44
		[](std::monostate)   { return MONOSTATE; },
45
		[](uint64_t u)       { return (u == one_of(0ull, 1ull)) ? BOOL : INTEGER; },
46
		[](double)           { return DOUBLE; },
47
		[](std::string_view) { return STRING; }
48
	});
49
	type = std::max(type, valueFormat);
50
	events.emplace_back(t, std::move(v));
51
}
52
53
void Tracer::Trace::clear()
54
{
55
	events.clear();
56
}
57
58
void Tracer::Trace::attachProbe(Debugger& debugger, ProbeBase& probe)
59
{
60
	probe.attach(*this);
61
	motherBoard = &debugger.getMotherBoard();
62
	description = probe.getDescription();
63
}
64
65
void Tracer::Trace::detachProbe(ProbeBase& probe)
66
{
67
	probe.detach(*this);
68
	motherBoard = nullptr;
69
	description.clear();
70
}
71
72
void Tracer::Trace::update(const ProbeBase& subject) noexcept
73
{
74
	assert(motherBoard);
75
	if (motherBoard->getReverseManager().isReplaying()) {
76
		return;
77
	}
78
	addEvent(motherBoard->getCurrentTime(), subject.getTraceValue(), false);
79
}
80
81
void Tracer::probeCreated(Debugger& debugger, ProbeBase& probe)
82
{
83
	if (auto* trace = findTrace(probe.getName())) {
84
		trace->attachProbe(debugger, probe);
85
	}
86
}
87
88
void Tracer::probeRemoved(ProbeBase& probe)
89
{
90
	if (auto* trace = findTrace(probe.getName())) {
91
		trace->detachProbe(probe);
92
	}
93
}
94
95
void Tracer::selectProbe(Debugger& debugger, std::string_view name)
96
{
97
	(void)getOrCreateTrace(debugger, name);
98
}
99
100
void Tracer::unselectProbe(Debugger& debugger, std::string_view name)
101
{
102
	dropTrace(debugger, name);
103
}
104
105
void Tracer::transfer(Debugger& oldDebugger, Debugger& newDebugger)
106
{
107
	assert(traces.empty());
108
	auto& oldTracer = oldDebugger.getTracer();
109
	for (auto& trace : oldTracer.traces) {
110
		if (trace->isProbe()) {
111
			if (auto* oldProbe = oldDebugger.findProbe(trace->name)) {
112
				trace->detachProbe(*oldProbe);
113
			}
114
			if (auto* newProbe = newDebugger.findProbe(trace->name)) {
115
				trace->attachProbe(newDebugger, *newProbe);
116
			}
117
		}
118
		traces.push_back(std::move(trace));
119
	}
120
	oldTracer.traces.clear();
121
}
122
123
void Tracer::stopReplay(EmuTime time) noexcept
124
{
125
	// when replay stops, drop all future events
126
	for (auto& trace : traces) {
127
		auto it = std::ranges::lower_bound(trace->events, time, {}, &Event::time);
128
		trace->events.erase(it, trace->events.end());
129
	}
130
}
131
132
Tracer::Trace& Tracer::getOrCreateTrace(Debugger& debugger, std::string_view name)
133
{
134
	Trace* trace = [&] {
135
		auto it = std::ranges::lower_bound(traces, name, {}, &Trace::name);
136
		if (it != traces.end() && (*it)->name == name) {
137
			return it->get();
138
		} else {
139
			it = traces.insert(it, std::make_unique<Trace>(std::string(name)));
140
			return it->get();
141
		}
142
	}();
143
	if (auto* probe = debugger.findProbe(name)) {
144
		trace->attachProbe(debugger, *probe);
145
	}
146
	return *trace;
147
}
148
149
Tracer::Trace* Tracer::findTrace(std::string_view name)
150
{
151
	auto it = std::ranges::lower_bound(traces, name, {}, &Trace::name);
152
	return (it != traces.end() && (*it)->name == name) ? it->get() : nullptr;
153
}
154
155
void Tracer::execute(Debugger& debugger, std::span<const TclObject> tokens, TclObject& result, EmuTime time)
156
{
157
	auto& cmd = debugger.cmd;
158
	cmd.checkNumArgs(tokens, Completer::AtLeast{3}, "subcommand ?arg ...?");
159
	cmd.executeSubCommand(tokens[2].getString(),
160
		"add",   [&]{ add(debugger, tokens, result, time); },
161
		"list",  [&]{ list(cmd, tokens, result); },
162
		"drop",  [&]{ drop(debugger, tokens, result); },
163
		"probe", [&]{ probe(debugger, tokens, result); });
164
}
165
166
void Tracer::add(Debugger& debugger, std::span<const TclObject> tokens, TclObject& /*result*/, EmuTime time)
167
{
168
	if (debugger.getMotherBoard().getReverseManager().isReplaying()) {
169
		return;
170
	}
171
172
	std::optional<std::string_view> description;
173
	std::optional<std::string_view> type;
174
	std::optional<std::string_view> format;
175
	bool merge = false;
176
	std::array info = {
177
		valueArg("-description", description),
178
		valueArg("-type", type),
179
		valueArg("-format", format),
180
		flagArg("-merge", merge),
181
	};
182
	auto& cmd = debugger.cmd;
183
	auto& interp = cmd.getInterpreter();
184
	auto arguments = parseTclArgs(interp, tokens.subspan(3), info);
185
	if (arguments.size() != one_of(size_t(1), size_t(2))) {
186
		interp.wrongNumArgs(3, tokens, "name ?value?");
187
	}
188
189
	Trace& trace = getOrCreateTrace(debugger, arguments[0].getString());
190
	if (description) {
191
		trace.description = *description;
192
	}
193
	if (type) {
194
		using enum Tracer::Trace::Type;
195
		if      (*type == "void")   trace.type = MONOSTATE;
196
		else if (*type == "bool")   trace.type = BOOL;
197
		else if (*type == "int")    trace.type = INTEGER;
198
		else if (*type == "double") trace.type = DOUBLE;
199
		else if (*type == "string") trace.type = STRING;
200
		else throw CommandException(
201
			"Invalid value for -type: ", *type,
202
			", must be one of void, bool, int, double, string");
203
	}
204
	if (format) {
205
		using enum Tracer::Trace::Format;
206
		if      (*format == "bin") trace.format = BIN;
207
		else if (*format == "dec") trace.format = DEC;
208
		else if (*format == "hex") trace.format = HEX;
209
		else throw CommandException(
210
			"Invalid value for -format: ", *format,
211
			", must be one of bin, dec, hex");
212
	}
213
214
	TraceValue value = [&]{
215
		if (arguments.size() < 2) {
216
			return TraceValue{std::monostate{}};
217
		}
218
		const auto& val = arguments[1];
219
		if (auto i = val.getOptionalInt()) {
220
			return TraceValue{uint64_t(*i)};
221
		} else if (auto i64 = val.getOptionalInt64()) {
222
			return TraceValue{uint64_t(*i64)};
223
		} else if (auto d = val.getOptionalDouble()) {
224
			return TraceValue{*d};
225
		} else {
226
			return TraceValue{val.getString()};
227
		}
228
	}();
229
	trace.addEvent(time, std::move(value), merge);
230
}
231
232
[[nodiscard]] static TclObject toTclObject(const TraceValue& value)
233
{
234
	return value.visit(overloaded{
235
		[](std::monostate)     { return TclObject(); },
236
		[](uint64_t u)         { return TclObject(u); },
237
		[](double d)           { return TclObject(d); },
238
		[](std::string_view s) { return TclObject(s); }
239
	});
240
}
241
242
void Tracer::list(Command& cmd, std::span<const TclObject> tokens, TclObject& result)
243
{
244
	cmd.checkNumArgs(tokens, Completer::Between{3, 4}, "?name?");
245
	if (tokens.size() == 3) {
246
		// list all traces
247
		result.addListElements(std::views::transform(traces, &Trace::name));
248
	} else {
249
		// list specific trace
250
		std::string_view name = tokens[3].getString();
251
		const Trace* trace = findTrace(name);
252
		if (!trace) {
253
			throw CommandException("No such trace: ", name);
254
		}
255
		for (const auto& event : trace->events) {
256
			result.addListElement(makeTclList(
257
				event.time.toDouble(),
258
				toTclObject(event.value)));
259
		}
260
	}
261
}
262
263
void Tracer::drop(Debugger& debugger, std::span<const TclObject> tokens, TclObject& /*result*/)
264
{
265
	auto& cmd = debugger.cmd;
266
	cmd.checkNumArgs(tokens, Completer::Between{3, 4}, "?name?");
267
	if (tokens.size() == 3) {
268
		// drop all traces
269
		traces.clear();
270
	} else {
271
		// drop specific trace
272
		dropTrace(debugger, tokens[3].getString());
273
	}
274
}
275
276
void Tracer::dropTrace(Debugger& debugger, std::string_view name)
277
{
278
	auto it = std::ranges::lower_bound(traces, name, {}, &Trace::name);
279
	if (it == traces.end() || (*it)->name != name) {
280
		// not found, not an error
281
		return;
282
	}
283
	if ((*it)->isProbe()) {
284
		if (auto* probe = debugger.findProbe(name)) {
285
			(*it)->detachProbe(*probe);
286
		}
287
	}
288
	traces.erase(it);
289
}
290
291
void Tracer::probe(Debugger& debugger, std::span<const TclObject> tokens, TclObject& /*result*/)
292
{
293
	auto& cmd = debugger.cmd;
294
	cmd.checkNumArgs(tokens, 4, "name");
295
	Trace& trace = getOrCreateTrace(debugger, tokens[3].getString());
296
	(void)trace;
297
}
298
299
void Tracer::tabCompletion(const Debugger& debugger, std::vector<std::string>& tokens) const
300
{
301
	static constexpr std::array cmds = {
302
		"add"sv, "list"sv, "drop"sv, "probe"sv,
303
	};
304
	auto& cmd = debugger.cmd;
305
	if (tokens.size() == 3) {
306
		cmd.completeString(tokens, cmds);
307
	} else if (tokens.size() == 4) {
308
		if (tokens[2] == one_of("add"sv, "list"sv, "drop"sv)) {
309
			cmd.completeString(tokens, std::views::transform(traces, &Trace::name));
310
		} else if (tokens[2] == "probe"sv) {
311
			cmd.completeString(tokens, std::views::transform(debugger.probes, &ProbeBase::getName));
312
		}
313
	}
314
}
315
316
[[nodiscard]] std::string Tracer::help(std::span<const TclObject> tokens) const
317
{
318
	constexpr auto generalHelp =
319
		"debug trace <subcommand> [<arguments>]\n"
320
		"  Possible subcommands are:\n"
321
		"    add    add a new trace data point\n"
322
		"    list   list the created traces\n"
323
		"    drop   drop trace data\n"
324
		"    probe  start collecting probe changes\n"
325
		"  The arguments are specific for each subcommand.\n"
326
		"  Type 'help debug trace <subcommand>' for help about a specific subcommand.\n";
327
328
	constexpr auto addHelp =
329
		"debug trace add <name> [<value>] [-merge] [-description <text>] [-type <t>] [-format <f>]\n"
330
		"  Add a new trace data point to the trace with the given name.\n"
331
		"  If the trace does not exist yet, it will be created.\n"
332
		"  The timestamp of the data point is the current emulation time.\n"
333
		"  The optional value can be an integer, a floating point number,\n"
334
		"  or a string. If no value is given, a void value is used.\n"
335
		"\n"
336
		"  There are also a few flags that influence the behavior:\n"
337
		"  * -merge: ignore consecutive equal values\n"
338
		"  * -description <text>: gives a description for this trace\n"
339
		"  * -type <t>: must be one of: void, bool, int, double, string\n"
340
		"               E.g. this ensures that a string like '1' is not accidentally interpreted as a bool\n"
341
		"  * -format <f>: must be one of: bin, dec, hex\n"
342
		"                 only relevant for type=int\n"
343
		"  The latter 3 options are typically only used once. The first is typically repeated on every invocation.";
344
345
	constexpr auto listHelp =
346
		"debug trace list [<name>]\n"
347
		"  Without arguments, list the names of all created traces.\n"
348
		"  With a name argument, list all data points of the given trace.\n"
349
		"  Each data point consists of a timestamp and a value.\n"
350
		"  The timestamp is given as the time (in seconds) since emulation start.\n"
351
		"  Data points are listed in chronological order.\n";
352
353
	constexpr auto dropHelp =
354
		"debug trace drop [<name>]\n"
355
		"  Without arguments, drop all traces and their data points.\n"
356
		"  With a name argument, drop only the trace with the given name.\n";
357
358
	constexpr auto probeHelp =
359
		"debug trace probe <name>\n"
360
		"  Start collecting changes for the given probe. As if on each change there's an automatic 'debug trace add <name> <value>'.\n";
361
362
	constexpr auto unknownHelp =
363
		"Unknown subcommand, use 'help debug trace' to see a list of valid subcommands.\n";
364
365
	auto size = tokens.size();
366
	assert(size >= 2);
367
	if (size == 2) {
368
		return generalHelp;
369
	} else if (tokens[2] == "add") {
370
		return addHelp;
371
	} else if (tokens[2] == "list") {
372
		return listHelp;
373
	} else if (tokens[2] == "drop") {
374
		return dropHelp;
375
	} else if (tokens[2] == "probe") {
376
		return probeHelp;
377
	} else {
378
		return unknownHelp;
379
	}
380
}
381
382
void Tracer::exportVCD(zstring_view filename)
383
{
384
	// k-way merge using a min-heap over traces to avoid collecting & sorting all events
385
	std::vector<size_t> idx(traces.size(), 0);
386
	using HeapEntry = std::pair<uint64_t, size_t>; // time, trace
387
	std::priority_queue<HeapEntry, std::vector<HeapEntry>, std::greater<HeapEntry>> heap;
388
	for (size_t i = 0; i < traces.size(); ++i) {
389
		const auto& t = *traces[i];
390
		if (!t.events.empty()) {
391
			heap.emplace(t.events.front().time.toUint64(), i);
392
		}
393
	}
394
395
	// open file and stream output
396
	std::ofstream os(filename.c_str());
397
	if (!os) throw MSXException("Cannot open file for writing: ", filename);
398
399
	// header
400
	os << "$date " << Date::toString(time(nullptr)) << " $end\n"
401
	      "$version openMSX Tracer exportVCD $end\n"
402
	      "$timescale 291005 fs $end\n"
403
	      "$scope module top $end\n";
404
405
	// declare variables
406
	for (size_t i = 0; i < traces.size(); ++i) {
407
		const auto& t = *traces[i];
408
		os << "$var ";
409
		switch (t.getType()) {
410
			using enum Tracer::Trace::Type;
411
			case MONOSTATE: os << "event 1";    break;
412
			case BOOL:      os << "wire 1";     break;
413
			case INTEGER:   os << "integer 64"; break;
414
			case DOUBLE:    os << "real 64";    break;
415
			case STRING:    os << "string 1";   break;
416
		}
417
		os << " t" << i << ' ' << t.name << " $end\n";
418
	}
419
420
	os << "$upscope $end\n"
421
	      "$enddefinitions $end\n";
422
423
	std::string tmpBuf;
424
	auto escapeString = [&](std::string_view str) {
425
		auto needEscape = [](char c) { return c == '\\' || c <= 32 || c >= 127; };
426
		auto it = std::ranges::find_if(str, needEscape);
427
		if (it == str.end()) return str; // fast path
428
429
		tmpBuf.assign(str.begin(), it);
430
		do {
431
			char c = *it;
432
			if (needEscape(c)) {
433
				strAppend(tmpBuf, "\\x", hex_string<2>(c));
434
			} else {
435
				tmpBuf += c;
436
			}
437
		} while (++it != str.end());
438
		return std::string_view{tmpBuf};
439
	};
440
441
	// helper: emit a value for given trace format. For events, 'flag' selects 1 (true) or 0 (false).
442
	auto emitValue = [&](Tracer::Trace::Type type, size_t id, const TraceValue& val, bool flag) {
443
		switch (type) {
444
		using enum Tracer::Trace::Type;
445
		case MONOSTATE:
446
			os << (flag ? '1' : '0') << 't' << id << '\n';
447
			break;
448
		case BOOL: {
449
			uint64_t v = val.get_if<uint64_t>().value_or(0);
450
			os << (v ? '1' : '0') << 't' << id << '\n';
451
			break;
452
		}
453
		case INTEGER: {
454
			uint64_t v = val.get_if<uint64_t>().value_or(0);
455
			os << 'b' << bin_string(v) << " t" << id << '\n';
456
			break;
457
		}
458
		case DOUBLE: {
459
			os << 'r';
460
			val.visit(overloaded{
461
				[&](std::monostate)   { os << '0'; },
462
				[&](uint64_t u)       { os << u; },
463
				[&](double d)         { os << std::format("{:.17g}", d); },
464
				[&](std::string_view) { os << '0'; } // this should not happen
465
			});
466
			os << " t" << id << '\n';
467
			break;
468
		}
469
		case STRING: {
470
			os << "s";
471
			val.visit(overloaded{
472
				[] (std::monostate)      { /* nothing */ },
473
				[&](uint64_t u)          { os << u; },
474
				[&](double d)            { os << std::format("{:.17g}", d); },
475
				[&](std::string_view sv) { os << escapeString(sv); }
476
			});
477
			os << " t" << id << '\n';
478
			break;
479
		}
480
		}
481
	};
482
483
	// write initial values in a $dumpvars block at time 0
484
	os << "#0\n"
485
	      "$dumpvars\n";
486
	for (size_t i = 0; i < traces.size(); ++i) {
487
		const auto& t = *traces[i];
488
		TraceValue initVal = std::monostate{};
489
		if (!t.events.empty()) {
490
			uint64_t firstNs = t.events.front().time.toUint64();
491
			if (firstNs == 0) initVal = t.events.front().value;
492
		}
493
		emitValue(t.getType(), i, initVal, false);
494
	}
495
	os << "$end\n";
496
497
	// iterate changes via k-way merge heap
498
	uint64_t curTime = UINT64_MAX;
499
	while (!heap.empty()) {
500
		auto [time, tr] = heap.top();
501
		heap.pop();
502
		if (time != curTime) {
503
			curTime = time;
504
			os << '#' << time << '\n';
505
		}
506
507
		size_t pos = idx[tr];
508
		const auto& t = *traces[tr];
509
		const auto& val = t.events[pos].value;
510
		emitValue(t.getType(), tr, val, true);
511
512
		// advance index and push next event for this trace
513
		++idx[tr];
514
		if (idx[tr] < t.events.size()) {
515
			heap.emplace(t.events[idx[tr]].time.toUint64(), tr);
516
		}
517
	}
518
}
519
520
} // namespace openmsx