diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 75207b7..d0be7a2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -54,11 +54,12 @@ jobs:
- name: Create PTIX
run: |
- $dirs = @{
+ # LTTngDriver is a command-line tool, not a WPA plugin: it ships the SDK and engine, so it can't be packed (its
+ # build output also carries the LTTng plugin's manifest). Its files are published by the next step.
+ $dirs = [ordered]@{
"PerfDataExtension" = "PerfDataExtension/bin/Release/netstandard2.1/"
"PerfDataTxtExtension" = "PerfDataTxtExtension/bin/Release/netstandard2.1/"
"LTTngDataExtensions" = "LTTngDataExtensions/bin/Release/netstandard2.1/"
- "LTTngDriver" = "LTTngDriver/bin/Release/net8.0/"
"Perfetto" = "PerfettoCds/bin/Release/netstandard2.1/"
"Cloud-init" = "LinuxLogParsers/LinuxPlugins-MicrosoftPerformanceToolkSDK/Cloud-init/bin/Release/netstandard2.1/"
"Dmesg" = "LinuxLogParsers/LinuxPlugins-MicrosoftPerformanceToolkSDK/DmesgIsoLog/bin/Release/netstandard2.1/"
@@ -73,7 +74,7 @@ jobs:
$pluginManifestPath = "$sourcePath/pluginManifest.json"
if (!(Test-Path $pluginManifestPath)) {
- Write-Error "pluginManifest.json not found in $sourcePath"
+ throw "pluginManifest.json not found in $sourcePath"
}
$pluginManifest = Get-Content -Path $pluginManifestPath | ConvertFrom-Json
@@ -83,6 +84,11 @@ jobs:
$packageName = "$pluginId-$pluginVersion.ptix"
plugintool pack -s $sourcePath -o "$targetPath/$packageName"
+
+ # Only the last command's exit code would otherwise decide the step's result.
+ if ($LASTEXITCODE -ne 0) {
+ throw "plugintool pack failed for $plugin (exit code $LASTEXITCODE)"
+ }
}
- name: Copy files and prepare artifacts
diff --git a/CtfPlayback/EventStreams/CtfEvent.cs b/CtfPlayback/EventStreams/CtfEvent.cs
index 73aa195..0be1fbb 100644
--- a/CtfPlayback/EventStreams/CtfEvent.cs
+++ b/CtfPlayback/EventStreams/CtfEvent.cs
@@ -5,6 +5,7 @@
using System.Diagnostics;
using CtfPlayback.EventStreams.Interfaces;
using CtfPlayback.FieldValues;
+using CtfPlayback.Metadata;
using CtfPlayback.Metadata.Interfaces;
namespace CtfPlayback.EventStreams
@@ -25,6 +26,8 @@ public CtfEvent(PacketReader packetReader, ICtfMetadata metadata, ICtfPacket own
public ulong ByteOffsetWithinPacket { get; private set; }
+ public uint StreamId => this.owningPacket.StreamId;
+
public CtfTimestamp Timestamp { get; private set; }
public CtfFieldValue StreamDefinedEventHeader { get; private set; }
@@ -102,8 +105,7 @@ public void Read()
private void ReadStreamEventHeader()
{
- uint streamIndex = this.owningPacket.StreamId;
- var eventHeaderDescriptor = this.packetReader.Metadata.Streams[(int)streamIndex].EventHeader;
+ var eventHeaderDescriptor = this.packetReader.Metadata.GetStream(this.owningPacket.StreamId).EventHeader;
Debug.Assert(eventHeaderDescriptor != null);
StreamDefinedEventHeader = eventHeaderDescriptor.Read(this.packetReader);
@@ -111,8 +113,7 @@ private void ReadStreamEventHeader()
private void ReadStreamEventContext()
{
- uint streamIndex = this.owningPacket.StreamId;
- var eventContextDescriptor = this.packetReader.Metadata.Streams[(int)streamIndex].EventContext;
+ var eventContextDescriptor = this.packetReader.Metadata.GetStream(this.owningPacket.StreamId).EventContext;
if (eventContextDescriptor == null)
{
return;
diff --git a/CtfPlayback/EventStreams/CtfPacket.cs b/CtfPlayback/EventStreams/CtfPacket.cs
index a7c9d00..39a59f5 100644
--- a/CtfPlayback/EventStreams/CtfPacket.cs
+++ b/CtfPlayback/EventStreams/CtfPacket.cs
@@ -6,6 +6,7 @@
using System.IO;
using CtfPlayback.EventStreams.Interfaces;
using CtfPlayback.FieldValues;
+using CtfPlayback.Metadata;
using CtfPlayback.Metadata.Interfaces;
namespace CtfPlayback.EventStreams
@@ -61,10 +62,12 @@ public bool ReadPacketMetadata()
this.PacketByteOffset = this.packetReader.CountOfBytesProcessed;
ReadPacketHeader();
- ReadPacketContext();
+ // The packet context layout depends on the stream, which is identified in the packet header.
this.DetermineStreamIndex();
+ ReadPacketContext();
+
this.packetReader.SetPacketSize(this.packetReader.PlaybackCustomization.GetBitsInPacket(this));
this.BitsInPacketContent =
@@ -116,7 +119,7 @@ private void ReadPacketHeader()
private void ReadPacketContext()
{
- this.StreamPacketContext = this.Metadata.Streams[(int)this.StreamId].PacketContext.Read(this.packetReader) as CtfStructValue;
+ this.StreamPacketContext = this.Metadata.GetStream(this.StreamId).PacketContext.Read(this.packetReader) as CtfStructValue;
}
private ulong GetRemainingBits()
@@ -135,6 +138,7 @@ private void DetermineStreamIndex()
if (streamIdDescriptor == null)
{
this.StreamId = 0;
+ return;
}
bool streamIdFound = this.TracePacketHeader.FieldsByName.TryGetValue("stream_id", out var streamIdFieldValue);
diff --git a/CtfPlayback/EventStreams/Interfaces/ICtfEvent.cs b/CtfPlayback/EventStreams/Interfaces/ICtfEvent.cs
index c80a429..3775772 100644
--- a/CtfPlayback/EventStreams/Interfaces/ICtfEvent.cs
+++ b/CtfPlayback/EventStreams/Interfaces/ICtfEvent.cs
@@ -16,6 +16,11 @@ public interface ICtfEvent
///
ulong ByteOffsetWithinPacket { get; }
+ ///
+ /// Id of the stream (stream class) that contains this event.
+ ///
+ uint StreamId { get; }
+
///
/// Timestamp associated with the event
///
diff --git a/CtfPlayback/Metadata/AntlrParser/CtfAntlrMetadataParser.cs b/CtfPlayback/Metadata/AntlrParser/CtfAntlrMetadataParser.cs
index c3cc665..fdc7eeb 100644
--- a/CtfPlayback/Metadata/AntlrParser/CtfAntlrMetadataParser.cs
+++ b/CtfPlayback/Metadata/AntlrParser/CtfAntlrMetadataParser.cs
@@ -45,6 +45,16 @@ public ICtfMetadata Parse(Stream metadataStream)
{
string metadata = this.GetMetadata(metadataStream);
+ return this.ParseText(metadata);
+ }
+
+ ///
+ /// Parse CTF metadata text that is not packetized.
+ ///
+ /// Metadata text
+ /// Metadata parsed from the text
+ internal ICtfMetadata ParseText(string metadata)
+ {
this.parser = GetContext(metadata);
// Error listeners can be switched out for debugging purposes. Leaving these comments here for easy access.
@@ -69,7 +79,7 @@ public ICtfMetadata Parse(Stream metadataStream)
/// String representation of the metadata
private unsafe string GetMetadata(Stream metadataStream)
{
- // See https://diamon.org/ctf/#spec7.1
+ // See https://diamon.org/ctf/v1.8.3/#spec7.1
byte[] headerBuffer = new byte[Marshal.SizeOf(typeof(MetadataPacketHeader))];
byte[] buffer = null;
diff --git a/CtfPlayback/Metadata/Ctf2/Ctf2Json.cs b/CtfPlayback/Metadata/Ctf2/Ctf2Json.cs
new file mode 100644
index 0000000..9e0cf89
--- /dev/null
+++ b/CtfPlayback/Metadata/Ctf2/Ctf2Json.cs
@@ -0,0 +1,273 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+namespace CtfPlayback.Metadata.Ctf2
+{
+ ///
+ /// A JSON number, kept in its textual form so that 64-bit unsigned values are not truncated.
+ ///
+ internal sealed class Ctf2JsonNumber
+ {
+ internal Ctf2JsonNumber(string text)
+ {
+ this.Text = text;
+ }
+
+ internal string Text { get; }
+
+ internal bool IsNegative => this.Text.StartsWith("-", StringComparison.Ordinal);
+
+ internal long AsLong() => long.Parse(this.Text, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture);
+
+ internal ulong AsULong() => ulong.Parse(this.Text, NumberStyles.None, CultureInfo.InvariantCulture);
+
+ public override string ToString() => this.Text;
+ }
+
+ ///
+ /// Minimal JSON reader used for CTF 2 metadata. Objects are returned as
+ /// of string to object, arrays as of object,
+ /// numbers as , and strings, booleans and null as their .NET equivalents.
+ ///
+ internal sealed class Ctf2Json
+ {
+ private readonly string text;
+ private int position;
+
+ private Ctf2Json(string text)
+ {
+ this.text = text;
+ }
+
+ internal static object Parse(string text)
+ {
+ var reader = new Ctf2Json(text);
+ reader.SkipWhitespace();
+ var value = reader.ReadValue();
+ reader.SkipWhitespace();
+ if (reader.position != text.Length)
+ {
+ throw reader.Error("Unexpected trailing characters");
+ }
+
+ return value;
+ }
+
+ private object ReadValue()
+ {
+ if (this.position >= this.text.Length)
+ {
+ throw this.Error("Unexpected end of JSON");
+ }
+
+ char c = this.text[this.position];
+ switch (c)
+ {
+ case '{':
+ return this.ReadObject();
+ case '[':
+ return this.ReadArray();
+ case '"':
+ return this.ReadString();
+ case 't':
+ this.Expect("true");
+ return true;
+ case 'f':
+ this.Expect("false");
+ return false;
+ case 'n':
+ this.Expect("null");
+ return null;
+ default:
+ if (c == '-' || (c >= '0' && c <= '9'))
+ {
+ return this.ReadNumber();
+ }
+
+ throw this.Error($"Unexpected character '{c}'");
+ }
+ }
+
+ private Dictionary ReadObject()
+ {
+ var result = new Dictionary(StringComparer.Ordinal);
+ this.position++;
+ this.SkipWhitespace();
+ if (this.TryConsume('}'))
+ {
+ return result;
+ }
+
+ while (true)
+ {
+ this.SkipWhitespace();
+ if (this.position >= this.text.Length || this.text[this.position] != '"')
+ {
+ throw this.Error("Expected property name");
+ }
+
+ string name = this.ReadString();
+ this.SkipWhitespace();
+ if (!this.TryConsume(':'))
+ {
+ throw this.Error("Expected ':'");
+ }
+
+ this.SkipWhitespace();
+ result[name] = this.ReadValue();
+ this.SkipWhitespace();
+
+ if (this.TryConsume(','))
+ {
+ continue;
+ }
+
+ if (this.TryConsume('}'))
+ {
+ return result;
+ }
+
+ throw this.Error("Expected ',' or '}'");
+ }
+ }
+
+ private List