Atom Tools: remove unnecessary Asset Processor work from a graph edit, and bound the wait - #20109
Open
Grimwarrior wants to merge 15 commits into
Open
Grimwarrior wants to merge 15 commits into
Grimwarrior wants to merge 15 commits into
Conversation
MoveFileWithTimeout retried a failed product replacement every 250 ms. The first failure is usually not contention that needs waiting out: immediately before removing a product the Asset Processor broadcasts JobFileClaimed to every connected client, and AzFramework's handler answers by *queuing* a streamer FlushCache. The handle is dropped a few milliseconds later on the streamer thread and nothing acknowledges it back, so the remove races that flush and loses, and a flat quarter second then charges full price for a wait that was over almost at once. With an editor open on the assets being built this is the normal path rather than an exceptional one, at 250 ms per product. Retries now start at 5 ms and double to the same 250 ms ceiling. A file that really is held open by something else reaches the old pacing within a few hundred milliseconds, while the common case costs about as little as the platform's sleep granularity allows. The overall timeout is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Grimwarrior <143121582+Grimwarrior@users.noreply.github.com>
…include
GetListOfIncludedFiles resolves #include lines textually, so it cannot follow an
include whose path is a macro. Material pipeline generated shaders reach their
parameter struct exactly that way. MaterialTypeBuilder emits
#define MATERIAL_PARAMETERS_AZSLI_FILE_PATH "<name>_parameters.azsli"
into the generated AZSL, and the shared material azsli then does
#include MATERIAL_PARAMETERS_AZSLI_FILE_PATH.
Without a dependency on that file the Asset Processor does not know the two
belong together. The generated surface evaluation azsli next to the graph is a
plain include and therefore is a registered dependency, so editing a graph
retriggers this shader job immediately, racing the Material Type Builder
pipeline stage job that regenerates the parameter struct. The shader then
compiles new graph code against an old struct and fails with "no member named
... in 'MaterialParameters'".
The dependency is registered by reading the define out of the generated AZSL, so
it is scoped to shaders that actually contain it and hand written shaders are
unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Grimwarrior <143121582+Grimwarrior@users.noreply.github.com>
SortNodesInExecutionOrder scored nodes by input slots, output slots and depth, then sorted stably. Those components tie readily -- two nodes with the same slot shape at the same depth is the ordinary case for sibling branches -- and a stable sort leaves ties in whatever order the caller supplied. Callers build their containers by iterating GraphModel::Graph::GetNodes(), which is an unordered_map, so inserting any node rehashes it and siblings come back the other way round. The generated shader source then changes line order without the graph meaning anything different. The Asset Processor sees a changed file and rebuilds every shader generated for the material, so dropping an unconnected node on the canvas was enough to trigger a full recompile. Adding the node ID as the last score component makes the ordering total, so the generated output depends only on the graph. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Grimwarrior <143121582+Grimwarrior@users.noreply.github.com>
GraphTemplateFileData::Save always wrote. Rewriting a file with identical content still updates its modification time and content hash, which makes the Asset Processor re-run every job that lists it as a source dependency -- including every shader generated for the material. Many graph edits regenerate identical text for most or all of the template files: moving a node, changing the selection, editing a branch that does not feed the output. Comparing against what is already on disk removes the entire asset pipeline round trip for that class of edit. Save gains an optional out parameter reporting whether it actually wrote, so a caller that needs to know what changed can ask rather than assume. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Grimwarrior <143121582+Grimwarrior@users.noreply.github.com>
…AP wait A graph compile runs on a background job and could not be stopped. A second edit arriving while one was in flight had to wait for it to finish, and the compiler could be left in Processing indefinitely when the Asset Processor never settled. Reset now reserves the compiler synchronously when idle, closing the window where a second edit observes a terminal state and dispatches another job before the first worker has started, and requests cancellation when a job is already active so the caller can leave the replacement queued. FinishCompile publishes exactly one terminal state and releases the reservation, with a cancellation request always winning over the requested state. CompileGraph still supports direct callers by creating and consuming its own reservation when none exists. The Asset Processor wait gains three things. Files with no builder are not waited on. An azsli has no jobs, so querying its status costs a full round trip that can never return anything but an empty list. ShouldReportGeneratedFileStatus is virtual so a derived compiler can also exclude files whose readiness its own consumer handles. The files stay in m_generatedFiles, which other systems read in full. The wait is bounded, 15 seconds by default and configurable through /O3DE/AtomToolsFramework/GraphCompiler/AssetStatusTimeoutMs, with 0 restoring the original unbounded behaviour. AssetStatusReporter walks its paths with an index that only moves forward, so a path it is sitting on has to reach a terminal job state or it waits on that path forever -- which the Asset Processor can fail to deliver, for instance when a duplicated intermediate entry (o3de#19642) leaves paths that never settle cleanly. Giving up reports the compile complete and leaves the consumer to pick the assets up from the catalog. AssetStatusReporter drains every path that has already settled per update rather than one. The driving thread sleeps 10 ms between iterations, so stepping a single path at a time imposed a floor of 10 ms times the number of reported paths on every compile, even when all of the jobs had long since finished. AssetStatusReporterSystem gains GetStatusMessage so the timeout warning can say which path it gave up on. Both GraphCompiler subclasses are converted to publish their terminal state through FinishCompile. The reservation is released there and nowhere else, so a subclass that reported Complete or Failed by calling SetState directly would hold the compiler reserved forever and never compile a second time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Grimwarrior <143121582+Grimwarrior@users.noreply.github.com>
Covers the contract the previous commit introduced, which is easy to get wrong from a subclass: a compile reserves the compiler, a second reservation attempt while one is active requests cancellation and leaves the replacement queued, a cancellation request wins over whatever terminal state the worker asks for, and the completion callback does not run for a compile that was cancelled. The last of those is the one worth a test. The reservation is released by FinishCompile and nowhere else, so a subclass that publishes its terminal state by calling SetState directly holds the compiler forever and never compiles a second time -- with no error, and no symptom until the tool stops responding to edits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Grimwarrior <143121582+Grimwarrior@users.noreply.github.com>
Contributor
|
This is incredible work, just please give us time to properly test and review. Changes to asset processing are very sensitive in that they can have a cascading failure effect. |
Contributor
Author
Yeah absolutely. No need to rush. This needs time to review. |
Signed-off-by: EnterTheArcane <96613937+EnterTheArcane@users.noreply.github.com>
Signed-off-by: EnterTheArcane <96613937+EnterTheArcane@users.noreply.github.com>
Signed-off-by: EnterTheArcane <96613937+EnterTheArcane@users.noreply.github.com>
Signed-off-by: EnterTheArcane <96613937+EnterTheArcane@users.noreply.github.com>
EnterTheArcane
self-requested a review
September 13, 2026 22:51
Signed-off-by: EnterTheArcane <96613937+EnterTheArcane@users.noreply.github.com>
Signed-off-by: EnterTheArcane <96613937+EnterTheArcane@users.noreply.github.com>
Signed-off-by: EnterTheArcane <96613937+EnterTheArcane@users.noreply.github.com>
Signed-off-by: EnterTheArcane <96613937+EnterTheArcane@users.noreply.github.com>
Signed-off-by: EnterTheArcane <96613937+EnterTheArcane@users.noreply.github.com>
Contributor
Author
|
@EnterTheArcane @nick-l-o3de hello sorry to bother you. So is it good to add all these PRS to development? There are still 2 pr I would like to add but they depend on these PRS . One is the material canvas preview and the other is the material canvas pane |
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.
AtomToolsFramework: stop a graph edit waiting on Asset Processor work that did not need doing
A graph edit in an Atom tool spends most of its time between the edit and the viewport waiting on the
Asset Processor. Much of that wait is for work that should never have been queued, and some of it is
sleep the code chooses rather than work anything is doing.
Six commits, independent of each other in effect but sharing one theme. Four remove or shorten a
wait, one fixes a build race that shows up as a hard shader compile failure, one adds a test.
The branch is self-contained: it depends on nothing else and nothing here needs a caller that does
not already exist.
Rebuilds that should not have been triggered
Generated output was not deterministic.
SortNodesInExecutionOrderscored nodes by input slots,output slots and depth, then sorted stably. Those components tie readily — two nodes with the same
slot shape at the same depth is the ordinary case for sibling branches — and a stable sort leaves
ties in whatever order the caller supplied. Callers build their containers by iterating
GraphModel::Graph::GetNodes(), anunordered_map, so inserting any node rehashes it and siblingscome back the other way round.
The generated shader source then changes line order without the graph meaning anything different,
the Asset Processor sees a changed file, and every shader generated for the material is rebuilt.
Dropping an unconnected node on the canvas was enough. Adding the node ID as the last score
component makes the ordering total, so the output depends only on the graph.
Unchanged files were rewritten anyway.
GraphTemplateFileData::Savealways wrote. Rewriting afile with identical content still moves its modification time and content hash, which re-runs every
job listing it as a source dependency — again, every shader generated for the material.
Many edits regenerate identical text for most or all of the template files: moving a node, changing
the selection, editing a branch that does not feed the output. Comparing against what is on disk
removes the entire asset pipeline round trip for that whole class of edit.
Savegains an optionalout parameter reporting whether it actually wrote, so a caller that needs to know can ask rather than
assume.
A dependency the Asset Processor could not see
GetListOfIncludedFilesresolves#includelines textually, so it cannot follow an include whosepath is a macro. Material pipeline generated shaders reach their parameter struct exactly that way:
MaterialTypeBuilderemits#define MATERIAL_PARAMETERS_AZSLI_FILE_PATH "<name>_parameters.azsli"and the shared material azsli does
#include MATERIAL_PARAMETERS_AZSLI_FILE_PATH.With no dependency registered, the Asset Processor does not know the two belong together. The
generated surface evaluation azsli beside the graph is a plain include and therefore is a
registered dependency, so editing a graph retriggers the shader job immediately — racing the
Material Type Builder pipeline stage job that regenerates the parameter struct. The shader compiles
new graph code against the old struct and fails with
no member named ... in 'MaterialParameters'.The dependency is registered by reading the define back out of the generated AZSL, so it is scoped to
shaders that actually contain it. Hand-written shaders are unaffected.
Waiting longer than the work takes
Product replacement retried on a flat 250 ms. The first failure is usually not contention that
needs waiting out. Immediately before removing a product the Asset Processor broadcasts
JobFileClaimedto every connected client, and AzFramework's handler answers by queuing a streamerFlushCache. The handle is dropped a few milliseconds later on the streamer thread and nothingacknowledges it back, so the remove races that flush and loses — and a flat quarter second charges
full price for a wait that was over almost immediately. With an editor open on the assets being
built this is the normal path, per product.
Retries now start at 5 ms and double to the same 250 ms ceiling. A file genuinely held open by
something else reaches the old pacing within a few hundred milliseconds; the common case costs about
as little as the platform's sleep granularity allows. The overall timeout is unchanged.
Files with no builder were waited on. An azsli has no jobs, so querying its status costs a full
round trip that can never return anything but an empty list.
ShouldReportGeneratedFileStatusisvirtual so a derived compiler can also exclude files whose readiness its own consumer handles. The
files stay in
m_generatedFiles, which other systems read in full.One settled path was drained per update. The driving thread sleeps 10 ms between iterations, so
stepping a single path at a time imposed a floor of 10 ms times the number of reported paths on every
compile, even when every job had long since finished. It now drains everything already settled.
The wait was unbounded.
AssetStatusReporterwalks its paths with an index that only movesforward, so a path it is sitting on must reach a terminal job state or it waits there forever — which
the Asset Processor can fail to deliver, for instance when a duplicated intermediate entry
(#19642) leaves paths that never settle. The wait is now bounded, 15 seconds by default and
configurable through
/O3DE/AtomToolsFramework/GraphCompiler/AssetStatusTimeoutMs, with0restoring the original unbounded behaviour. Giving up reports the compile complete and leaves the
consumer to pick the assets up from the catalog.
AssetStatusReporterSystemgainsGetStatusMessageso the warning can name the path it gave up on.Cancellable compilation
A graph compile ran on a background job and could not be stopped, so a second edit arriving mid-flight
had to wait out the first, and the compiler could sit in
Processingindefinitely if the AssetProcessor never settled.
Resetnow reserves the compiler synchronously when idle — closing the window where a second editobserves a terminal state and dispatches another job before the first worker has started — and
requests cancellation when a job is already active, so the caller can leave the replacement queued.
FinishCompilepublishes exactly one terminal state and releases the reservation, with acancellation request always winning over the requested state.
CompileGraphstill supports directcallers by creating and consuming its own reservation when none exists.
Both
GraphCompilersubclasses are converted to publish their terminal state throughFinishCompile. This is the part to review closely: the reservation is released there and nowhereelse, so a subclass reporting
CompleteorFailedby callingSetStatedirectly holds thecompiler reserved forever and never compiles again — with no error, and no symptom until the tool
stops responding to edits.
The final commit tests that contract: a compile reserves the compiler, a second reservation attempt
requests cancellation and leaves the replacement queued, a cancellation request wins over the
worker's terminal state, and the completion callback does not run for a cancelled compile.
Compatibility
stable from then on.
is unchanged.
0restores the previous unbounded behaviour.