-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Capture code snippet from diagnostic compiler output #9362
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
* Built tentative test class SwiftBuildSystemOutputParser to handle the compiler output specifically * Added a handleDiagnostic method to possibly substitute the emitEvent local scope implementation of handling a SwiftBuildMessage diagnostic
* the flag `appendToOutputStream` helps us to determine whether a diagnostic is to be emitted or whether we'll be emitting the compiler output via OutputInfo * separate the emitEvent method into the SwiftBuildSystemMessageHandler
a20f020 to
f3aaabf
Compare
1dcaaeb to
c48e606
Compare
|
@swift-ci please test |
owenv
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thie generally lgtm but I have some concerns about the regex-based parsing when we emit the textual compiler output.
- Perf - It's important this is fast so that it doesn't block the end of the build if a command produces huge quantities of output. It's hard to say if this will be a real issue without some testing
- We're re-parsing information which we're already getting from the compiler in structured form. I see the appeal of not reporting a diagnostic twice if multiple compile jobs report it though
|
|
||
| /// Split compiler output into individual diagnostic segments | ||
| /// Format: /path/to/file.swift:line:column: severity: message | ||
| private func splitIntoDiagnostics(_ output: String) -> [ParsedDiagnostic] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should be doing any output parsing at this level, Swift Build already has an entire subsystem dedicated to doing this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jakepetroules I would agree, however the events we're receiving from SwiftBuild accumulates every diagnostic message into a singular data buffer -- when emitting this as-is, it's possible that we'd be coupling some info-level diagnostics with error-level diagnostics with no way to separate the severity. Splitting the string on a per-diagnostic basis is the only way to achieve this in this way.
The observability scope that we use to emit these diagnostics will capture the entire string blob, and we have to decide with what severity to emit the entire message. I'm not sure if there's an alternative path here that would give us the same ergonomics on the user front, but IMO it's preferred to separate each diagnostic and ensure that we're emitting them with the appropriate severity, rather than sending the entire string of possibly many diagnostics with varying severities.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me try to clarify: Swift Build already parses the individual diagnostic messages into structured data objects independently of the singular output buffer, which is for the textual output of the tool. So Swift Build is already doing the equivalent of what splitIntoDiagnostics is doing, and those messages are given to you in the diagnostics message. They include rich information including severity, line number, and so on.
Similar to what I mentioned elsewhere about the taskStarted/taskEnded events, you want to capture those diagnostics' association to the specific task (e.g. unprocessedDiagnostics should be a dictionary rather than an array, and belongs in BuildState rather than in the top level object), and then replay them at task completion time, without attempting to do your own parsing.
I also pointed this out inline, though I'm not sure why the re-parsing relates to deduplication? We should be able to deduplicate diagnostics whether we parse them again or use the existing ones. |
(@jakepetroules @owenv -- tagging for visibility, GitHub notifications can be weird) Re-parsing doesn't affect the de-duplication -- when tracking the data buffer per task, I also track whether we've emitted the associated output for a given task (using its signature) and guard against this before emitting for that task again. We only go down this path (emitting for a given task) once we've received the task completed event. I mentioned this inline as well but for visibility: the re-parsing just addresses the fact that for a given task signature, we have an accumulated data buffer that contains all possible diagnostic messages coming from the compiler. I find that the user ergonomics aren't great when simply emitting the entire string blob through the observability scope, since these diagnostics can have varying severities and we'll have to decide up-front which severity to choose to emit the entire string of all diagnostics. I do also maintain a list of the Perhaps some more discussion is needed here. 😄 |
* Remove taskStarted outputStream emissions * Propagate exception if unable to find path
|
@swift-ci please test |
|
@swift-ci please test windows |
The TaskDataBuffer introduces some extra complexity in its handling of various buffers we'd actually like to track and emit, rather than ignore. We will keep multiple buffers depending on the information we have available i.e. whether there's an existing task signature buffer, taskID buffer, targetID buffer, or simply a global buffer.
If there is no command line display strings available for a failed task, we should demote this message to info-level to avoid cluttering the output stream with messages that may not be incredibly helpful. To consider here: if this is the only error, we should be able to expose this as an error and perhaps omit "<no command line>".
|
@swift-ci please test |
|
The failing tests seem to be command plugins that do things like this Not sure if we're failing to print the command line in the log, or the observabilityscope isn't adding it to logText |
| private var taskSignatureBuffer: [String: Data] = [:] | ||
| private var taskIDBuffer: [Int: Data] = [:] | ||
| private var targetIDBuffer: [Int: Data] = [:] | ||
| private var globalBuffer: Data = Data() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't look like we currently print this anywhere. Emitting it at the end of the build might work to start, but we might also consider printing up to each newline we see since some "build preparation" messages get emitted w/ no task and target
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realized that while we emit global diagnostics fairly often, the build system doesn't emit a lot of global raw output, so this may actually be ok as-is for now. I'm going to look at the build system to see if we can make changes to guarantee that raw output is always attached to a task
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After our conversation earlier I checked this as well and the BuildOperationConsoleOutputEmitted constructors for non-task output are never called (outside of a test). The only possible way output could be non-task-associated at all is through its Decodable initializer, and the sole place that's called is in the IPC decoding over the wire, so no chance because no such objects are constructed in the first place.
I think we should change the API to guarantee this, as global or target-attached console output doesn't make any sense anyways.
For now @bripeticca can just ignore it (meaning, let's eliminate the field from this PR).
| if buildSystem.enableTaskBacktraces { | ||
| buildState.collectedBacktraceFrames.add(frame: info) | ||
| } | ||
| case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .buildStarted, .preparationComplete, .targetUpToDate, .targetComplete, .taskUpToDate: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we emit any buffered target messages upon receiving targetComplete here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as #9362 (comment), this may be ok as-is
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, not used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to remove targets from the in-progress map when hitting targetComplete though.
| } | ||
|
|
||
| mutating func appendToBuffer(_ info: SwiftBuildMessage.OutputInfo) { | ||
| // Attempt to key by taskSignature; at times this may not be possible, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason this taskID vs taskSignature situation exists was because we started to transition from IDs (which change on every build operation) to signatures (which are stable across build operations), but didn't complete it, and also ran into performance issues (106579386) and turned them off for console output at least... I don't remember why and maybe we can re-enable them at some point, but we should definitely clean this up (not that it should block this PR though).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the clarification!
| try buildState.started(task: info) | ||
|
|
||
| if let commandLineDisplay = info.commandLineDisplayString { | ||
| self.observabilityScope.emit(info: "\(info.executionDescription)\n\(commandLineDisplay)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These log emissions still need to be deferred to the task complete event, like the diagnostics and output.
| private var buildState: BuildState = .init() | ||
|
|
||
| let progressAnimation: ProgressAnimationProtocol | ||
| var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] = [:] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be keyed by targetID, not target name. Target names are not globally unique.
| if buildSystem.enableTaskBacktraces { | ||
| buildState.collectedBacktraceFrames.add(frame: info) | ||
| } | ||
| case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .buildStarted, .preparationComplete, .targetUpToDate, .targetComplete, .taskUpToDate: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to remove targets from the in-progress map when hitting targetComplete though.
| public protocol DiagnosticsHandler: Sendable { | ||
| func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) | ||
|
|
||
| func printToOutput(message: String) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Call this func print(_ output: String, verbose: Bool) instead? Since we already have a viable implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's the DiagnosticHandlers responsibility to print the diagnostic here. We should instead have a "LoggingHandler" which would know where to emit the "message", whether it's to stdout, stderr, to a file, or some other place.
| if info.appendToOutputStream { | ||
| emitInfoAsDiagnostic(info: info) | ||
| } else { | ||
| unprocessedDiagnostics.append(info) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we don't need to save these at all. By definition these are the diagnostics which were parsed from the textual output and therefore the textual output will contain them.
| buildSystem.delegate?.buildSystem(buildSystem, didUpdateTaskProgress: message) | ||
| case .diagnostic(let info): | ||
| if info.appendToOutputStream { | ||
| emitInfoAsDiagnostic(info: info) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to capture task-level diagnostics and defer emission of those until the taskCompleted event.
Global diagnostics and target diagnostics can be emitted immediately.
bkhouri
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great work, but we should also include automated tests as part of this change to ensure we won't regress the behaviour.
| public protocol DiagnosticsHandler: Sendable { | ||
| func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) | ||
|
|
||
| func printToOutput(message: String) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue: the function name is a bit misleading. Where if the output going to? to a file, stdout, stderr, somewhere else? can we please update the function name to make this more explicit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is meant to be a temporary workaround given that we are emitting diagnostics from the compiler output accumulated as one string blob, and observability scope usually demands a severity along with this despite the fact that these diagnostics could very possibly have differing severities.
It's a little hacky but I'm hoping that future discussion surrounding how we'd like to architect logging will improve this :)
| public protocol DiagnosticsHandler: Sendable { | ||
| func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) | ||
|
|
||
| func printToOutput(message: String) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's the DiagnosticHandlers responsibility to print the diagnostic here. We should instead have a "LoggingHandler" which would know where to emit the "message", whether it's to stdout, stderr, to a file, or some other place.
| } | ||
|
|
||
| /// Convenience extensions to extract taskID and targetID from the LocationContext. | ||
| extension SwiftBuildMessage.LocationContext { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: Should these extension belong in SwiftBuild?
| } | ||
| } | ||
|
|
||
| /// Handler for SwiftBuildMessage events sent by the SWBBuildOperation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: can we move this class to it's own source file?
| /// Tracks the task IDs for failed tasks. | ||
| private var failedTasks: [Int] = [] | ||
| /// Tracks the tasks by their signature for which we have already emitted output. | ||
| private var tasksEmitted: Set<String> = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question (possibly-blocking): what is the difference between tasksEmitted and taskIDsEmitted? If they are tracking the same thing, can we remove one? If we need both, could you please provide an explanation?
This PR refactors diagnostic handling in the Swift build system by introducing a dedicated message handler and per-task output buffering to properly parse and emit compiler diagnostics individually.
Key Changes
SwiftBuildSystemMessageHandler
SwiftBuildMessageevents from the build operationPer-Task Data Buffering
taskDataBufferstruct inBuildStateto capture compiler output per task signatureTaskDataBufferstruct allows for usingLocationContextorLocationContext2as a subscript key to fetch the appropriate data buffer for a task, defaulting to the global buffer if no associated task or target can be identified..outputmessages arrive