TraceEvent Importer (managed) replacement for ReadTrace#515
Open
TraceEvent Importer (managed) replacement for ReadTrace#515
Conversation
…xe dependency in the future - Add `TraceEventImporter` project: managed importer plugin reading `.trc`/`.xel` files via XELite, normalizing SQL text, computing hash IDs, and bulk-loading into the `ReadTrace` schema - Embed `CreateSchema.sql` and `PostLoadFixups.sql` as assembly resources; includes `vwBatchPartialAggsByGroupTimeInterval` view matching ReadTrace.exe output format - Use ReadTrace.exe index naming convention (no `IX_` prefix) for compatibility with `ReadTracePostProcessing.sql` index hints - Add `ProjectReference` in `sqlnexus.csproj`, register importer in `AppConfig.xml` - Set `AppendTargetFrameworkToOutputPath=false` so output goes to `bin\Release\` directly - Implement mutual exclusivity between ReadTrace and TraceEventImporter: auto-disable ReadTrace at startup when both enabled, MessageBox notification on manual toggle - Restrict `SupportedMasks` to `*pssdiag*.xel` and `*LogScout*.xel` to avoid conflicts with CustomXELImporter - Declare `ReadTracePostProcessing.sql` as post-script; update `RunPostScripts` to allow it for both importers - Fix `BulkLoadRowset.GetNewRow()` to handle schema-qualified table names (`[ReadTrace].[tblName]` instead of `[ReadTrace.tblName]`) - Fix `ImportOptions` saved options overwrite bug: add `HasOption()` method, only restore saved values when they actually exist - Skip non-boolean options (e.g., `int`) from checkbox menu to prevent `InvalidCastException` - Add `EnforceTraceImporterExclusivity()` and `IsImporterEnabled()` helpers in `fmImport.cs`
…ty, import logging, and UI fixes - Fix XEL `cpu_time` unit conversion in `XelFileReader.cs`: divide by 1000 (microseconds → milliseconds) to match ReadTrace.exe convention - Wrap all `BulkLoadRowset` usages in `BulkWriter.cs` with `try/finally` blocks to ensure `Close()` is called on exception - Add per-table row count logging in `TraceEventImporterPlugin.cs` after import for diagnostics - Close dropdown menus via `cmOptions.Close()` before showing importer conflict MessageBox in `fmImport.cs` to prevent dialog from being hidden behind menus - Pass `this` as owner to `MessageBox.Show()` for proper modal centering - Remove `EnumFiles()` call from `tsiBool_Click` to prevent `NullReferenceException` when `instances` is not yet initialized
…eadTraceManaged-pijocoder-042726
Contributor
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
…dd volatile to _cancelled field - BulkLoadRowset.cs: Wrap SqlDataAdapter in using statement for proper disposal - BulkWriter.cs: Remove redundant (object)x ?? DBNull.Value checks for non-nullable value types (int, byte, long) in WriteBatches, WriteStatements, WriteInterestingEvents, WriteProcedureNames, WriteUniqueBatches - TraceEventImporterPlugin.cs: Mark _cancelled field as volatile to ensure cross-thread visibility and fix always-false condition in inner cancellation check
EventProcessor - align sequence and connection handling with ReadTrace.exe: - BatchSeq/StmtSeq now use Starting event's global sequence (no separate counters) - Statement→Batch linking uses in-flight _curBatchStartSeq session state - ConnSeq uses global sequence namespace for both real and fake connections - Add "CONNECTED BEFORE TRACE" placeholder rows for sessions with no login event - Only create fake connection rows from batch completion (not statements) - Retain _sessionConnSeq on logout to prevent duplicate fake connection rows
- Add SpExecuteSqlExtractor to extract inner SQL query from sp_executesql, sp_prepexec, sp_prepare, and sp_cursor* RPC calls for normalization/hashing (aligns with ReadTrace.exe GetRPCParamText behavior) - SqlTextNormalizer: only normalize auto-generated RPC params (@p0, @p1, ...) to @p#; preserve named parameters (@JOB_ID, @ALL, @IS_SYSTEM) as-is (aligns with ReadTrace.exe normalization)
…e to file format header. Not a common use case
Comment on lines
+56
to
+64
| if (i < len - 1 && (textData[i] == 'N' || textData[i] == 'n') && textData[i + 1] == '\'') | ||
| { | ||
| // Check if this is after the proc name (not part of it) | ||
| // by verifying we've passed at least one space/comma | ||
| if (i > 0 && IsAfterProcName(textData, i)) | ||
| { | ||
| return ExtractQuotedString(textData, i + 2); | ||
| } | ||
| } |
Comment on lines
+86
to
+97
| if (c == '0' && i + 1 < len && (sql[i + 1] == 'x' || sql[i + 1] == 'X')) | ||
| { | ||
| // Ensure it's not part of an identifier | ||
| if (i == 0 || !IsIdentChar(sql[i - 1])) | ||
| { | ||
| i += 2; | ||
| while (i < len && IsHexDigit(sql[i])) | ||
| i++; | ||
| AppendWithSpace(result, "{BS}"); | ||
| continue; | ||
| } | ||
| } |
|
|
||
| private static bool IsHexDigit(char c) | ||
| { | ||
| return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); |
| string prefix = ""; | ||
| while (i < sql.Length && char.IsLetter(sql[i])) | ||
| { | ||
| prefix += sql[i]; |
Comment on lines
+193
to
+197
| foreach (var ti in TimeIntervals) | ||
| { | ||
| if (t >= ti.StartTime && t < ti.EndTime) | ||
| return ti.TimeInterval; | ||
| } |
| if (xe.Actions != null && xe.Actions.TryGetValue(actionName, out object val) && val != null) | ||
| return val.ToString(); | ||
| } | ||
| catch { } |
| if (xe.Actions != null && xe.Actions.TryGetValue(actionName, out object val) && val != null) | ||
| return Convert.ToInt64(val); | ||
| } | ||
| catch { } |
| if (xe.Actions != null && xe.Actions.TryGetValue(actionName, out object val) && val != null) | ||
| return Convert.ToInt64(val); | ||
| } | ||
| catch { } |
Comment on lines
+275
to
+281
| catch (Exception ex) | ||
| { | ||
| LogMessage($"TraceEventImporter: Error - {ex.Message}"); | ||
| LogMessage(ex.ToString()); | ||
| State = ImportState.Idle; | ||
| return false; | ||
| } |
Comment on lines
+338
to
+349
| foreach (string batch in batches) | ||
| { | ||
| string trimmed = batch.Trim(); | ||
| if (trimmed.Length == 0) continue; | ||
|
|
||
| using (var cmd = new SqlCommand(trimmed, conn)) | ||
| { | ||
| cmd.CommandTimeout = 0; | ||
| cmd.ExecuteNonQuery(); | ||
| executedCount++; | ||
| } | ||
| } |
…rt returning no data tblUniqueBatches.Seq and tblUniqueStatements.Seq must store the global event sequence of the first occurrence of each unique batch/statement, matching the value in tblBatches.BatchSeq / tblStatements.StmtSeq. Previously, UniqueStore used auto-incremented local counters (++_uniqueBatchSeq / ++_uniqueStmtSeq) which produced small sequential values (1, 2, 3...) that never matched the large XEL event sequence numbers stored as BatchSeq/StmtSeq in the detail tables. This caused spReporter_ExampleBatchDetails (and related report procs) to return zero rows because the join `tblUniqueBatches.Seq = tblBatches.BatchSeq` could never be satisfied. Changes: - UniqueStore.TryAddBatch: add batchSeq parameter; use it as Seq - UniqueStore.TryAddStatement: add stmtSeq parameter; use it as Seq - EventProcessor.HandleBatchCompleted: pass batchSeq to TryAddBatch - EventProcessor.HandleStatementCompleted: declare stmtSeq before use, pass it to TryAddStatement - Remove unused _uniqueBatchSeq and _uniqueStmtSeq counters from UniqueStore
Added IsCurrencySymbol() helper covering the Unicode Currency Symbols
block (\u20A0-\u20CF) plus the Latin-1 extras $, ¢, £, ¤, ¥ — all
expressed as \uXXXX escapes to avoid encoding fragility.
In the main normalization loop, a currency symbol immediately followed
by a digit is now skipped before the numeric literal handler runs, so
values like $380, €50, or ¥100 are normalized to {##} the same as a
bare number. The guard on the numeric literal section is also relaxed
to allow digits preceded by a currency symbol, covering the case where
$ is otherwise treated as an identifier character.
String/character data is unaffected: the currency handler requires
char.IsDigit on the next character, and string literals are consumed
and continued well before this code is reached.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Introduction
Trace Event Importer is a fully managed SqlNexus importer plugin that reads SQL Server trace files (.trc and .xel) directly in C#, eliminating the dependency on the legacy native ReadTrace.exe executable. It normalizes SQL text, computes hash-based batch/statement groupings, and bulk-loads processed data into the existing ReadTrace database schema used by SqlNexus for workload analysis. It creates identical database schema and objects as those of ReadTrace to avoid having to recreate reports and queries.
Key Design Decisions
Same schema, same reports: The importer writes to the identical
ReadTrace.*tables that ReadTrace.exe creates. All existing RDLC reports work without modification.Embedded SQL scripts:
CreateSchema.sqlandPostLoadFixups.sqlare embedded resources in the DLL, eliminating external file dependencies.INexusImporter (not INexusFileImporter): The plugin handles its own file enumeration internally (same pattern as ReadTrace), receiving a file mask from the host and processing all matching files in a single
DoImport()call.XELite for XEL reading: Uses the
Microsoft.SqlServer.XEvent.XELiteNuGet package for reading Extended Events files, which handles the binary XEL format reliably.BulkLoadRowset for writing: Uses the existing
BulkLoadExlibrary (shared with other importers) for efficient bulk insert.Implementation Plan
Phase 1: Core Pipeline
TraceEvent.cs): Define a unified event structure that can represent both TRC and XEL events with fields for session, request, timestamps, duration, CPU, reads, writes, text data, etc.TrcFileReader.cs): Read SQL Trace binary format, map trace event IDs toTraceEventTypeenum, extract fields by column ID.XelFileReader.cs): Read Extended Events files via XELite, map XE event names (sql_batch_completed,rpc_completed, etc.) to the sameTraceEventTypeenum.EventProcessor.cs): Correlate Starting↔Completed events per session+request, track connections (login→logout), handle attention events.Phase 2: Normalization & Hashing
SqlTextNormalizer.cs): Replace string literals with{STR}, numeric literals with{##}, normalize whitespace, handle quoted identifiers.HashComputer.cs): Compute stable 64-bit hash IDs for normalized SQL text (must produce consistent hashes for grouping).SpecialProcDetector.cs): Identifysp_executesql,sp_prepare,sp_prepexec,sp_cursoropen, etc. and extract the inner SQL text for normalization.UniqueStore.cs): Deduplicate batches, statements, app names, login names, procedure names; assign sequential IDs.Phase 3: Aggregation & Database Writing
Aggregator.cs): Partition events into configurable time intervals (default 60 seconds), compute per-HashID partial aggregates (min/max/total for duration, reads, writes, CPU; counts for starting/completed/attention events).BulkWriter.cs): Write all ReadTrace tables viaBulkLoadRowset— metadata, reference data, unique text, fact tables, and aggregation tables.CreateSchema.sql): CREATE SCHEMA, all tables matching the ReadTrace.exe layout, and thevwBatchPartialAggsByGroupTimeIntervalview.PostLoadFixups.sql): Create primary keys, nonclustered indexes (using ReadTrace.exe naming convention for index hint compatibility), and reconcileConnSeq/BatchSeq/ParentStmtSeqforeign key relationships.Phase 4: Host Integration
ProjectReferenceinsqlnexus.csproj, register inAppConfig.xml.ReadTraceschema, only one can be active at a time.MessageBoxinforming the user when toggling between importers at runtimeSaveImportOptions*pssdiag*.xeland*LogScout*.xelmasks (not*.xel) to avoid picking up system health, AlwaysOn, or SQLDiag XEL files that belong to theCustomXELImporter.ReadTracePostProcessing.sqlas a post-script, and update the host'sRunPostScriptsto allow it for both ReadTrace and TraceEventImporter."Aggregation interval (seconds)"gracefully in the UI (skip from checkbox menu).false) when no saved options exist yet.Phase 5: Schema Compatibility
The following objects must match what
ReadTracePostProcessing.sqland the RDLC reports expect:ReadTrace.vwBatchPartialAggsByGroupTimeInterval— must return exactly 10 columns (StartTime,EndTime,TimeInterval,StartingEvents,CompletedEvents,Attentions,Duration,Reads,Writes,CPU) aggregated across all HashIDs per time interval.tblBatches_HashIDnotIX_tblBatches_HashID) becauseReadTracePostProcessing.sqlusesWITH (INDEX(...))hints that reference these exact names.ReadTracePostProcessing.sqlstored procedures and the RDLC report datasets expect.Phase 6: BulkLoadRowset Fix
BulkLoadRowset.GetNewRow()wraps the table name in[brackets], which breaks for schema-qualified names likeReadTrace.tblBatches(produces[ReadTrace.tblBatches]instead of[ReadTrace].[tblBatches]). Fix to split on.and bracket each part separately.Configuration
truetrue60Testing Plan
1. Build & Deploy Verification
TraceEventImporter.dllis present insqlnexus\bin\Release\CreateSchema.sql,PostLoadFixups.sql)2. Importer Discovery & UI
Enabledchecked"Aggregation interval (seconds)"option does NOT appear as a checkbox (non-boolean options are hidden)3. Basic Import (XEL files)
*LogScout*.xelfilesReadTraceschema and all tables are createdIX_prefix)ReadTrace.vwBatchPartialAggsByGroupTimeIntervalview existstblBatches,tblStatements,tblConnections,tblUniqueBatches, etc.4. Basic Import (TRC files)
*sp_trace*.trcfiles5. Report Validation
6. Comparative Testing (TraceEventImporter vs ReadTrace)
This is the critical validation — import the same trace files with both importers and compare results.
Setup
.xeland.trcif possible)sqlnexus_managedandsqlnexus_readtrace)sqlnexus_managedsqlnexus_readtraceComparison Queries
More test queries:
Expected Differences
Some minor differences are acceptable:
NormTextinstead{STR}vsN'{STR}')Red Flags (investigate if seen)
7. File Exclusion
log_1.trc,log_2.trc, and*_blk.trcfiles alongside valid trace files*pssdiag*and*LogScout*patterns)8. Re-import & Drop Tables
9. Disabled Importer
10. Cancellation