Optimize for large bundles with numerous globs - #326
Conversation
|
This should be re-worked to use GlobbingUrlBuilder - It internally uses a Cache and is backed by the full faith, credit and trust of ASP.NET Core :D |
|
I'll give it a whirl and let @madskristensen know but superficially makes sense. There's another issue in the Open Issues list where I looked into a somewhat related issue with file provider use cases. I personally think this code base needs a regression suite around that stuff. Might re-clone the repo and ask Claude to help think through OATS pair-wise tests of feature combinations. |
|
@jzabroski, found the bug: GlobbingUrlBuilder doesn't keep the original included files order, I'll restore the foreach loop and call the builder inside it. Of course, I'll add tests for this :) |
|
just let me know when this is ready to merge |
ceb456b to
ce5d20d
Compare
|
@jzabroski, @madskristensen, done |
@snipervld Thanks for putting this together; I didn't see where your tests explicitly ensured this original order is preserved? Otherwise, I added a minor comment about the MockFileInfo private constructor. I think the first parameter name should be directoryName rather than name, to reflect the fact it is just a helper for the CreateDirectory factory method. |
Oops, sorry, I fixed the logic, but forgot about them after the comment :) Add some more tests. @jzabroski, I didn't see your comments about MockFileInfo (maybe you didn't |
Used GlobbingUrlBuilder to find matching files in the file provider.
Added tests for nested file structure (there were tests just for plain
FSs).
Caveat: GlobbingUrlBuilder uses binary search, so the order of files
matching the glob pattern may differ from the previous implementation.
E.g. if the bundle is defined this way:
pipeline.AddJavaScriptBundle("bundle1",
"js/a.js", "js/x-*.js", "js/b.js", "js/c/js");
files within "js/x-*.js" group may have the different order.
|
Yes, there seems to be a weird glitch where I can't submit the review since im technically not marked as a reviewer. |
|
@jzabroski, what do you think about 2d5d828? |
jzabroski
left a comment
There was a problem hiding this comment.
@snipervld There's a few code clean-ups I recommend doing to make sure as other contributors build on top of your tests, they do not violate invariants in your test setups. Or, if they do, future PR reviews are easier to distinguish the changes and think critically about.
| context.Setup(c => c.RequestServices.GetService(typeof(IMemoryCache))) | ||
| .Returns(cache.Object); | ||
| context.SetupGet(c => c.Request.PathBase).Returns(pathBase); | ||
| context.SetupGet(c => c.Items).Returns(new Dictionary<object, object>()); |
There was a problem hiding this comment.
You should call Mock.VerifyAll(context, optionsFactory, optionsMonitorCache, cache, asset, assetPipeline); at the end of the method, where you perform Asserts.
| LastModified = lastModified; | ||
| } | ||
|
|
||
| private MockFileInfo(string name, DateTimeOffset lastModified, IList<MockFileInfo> files = null) |
There was a problem hiding this comment.
Rename the first parameter to directoryName; this makes it more intuitive that this private constructor is meant to serve as a back-end constructor for the CreateDirectory factory method.
| context.Verify(c => c.RequestServices.GetService(typeof(IWebHostEnvironment))); | ||
| context.Verify(c => c.RequestServices.GetService(typeof(IMemoryCache))); | ||
| context.Verify(c => c.Response.Headers); | ||
| env.Verify(e => e.WebRootFileProvider); |
There was a problem hiding this comment.
Why not use Mock.VerifyAll?
There was a problem hiding this comment.
I tried Mock.VerifyAll, but it failed, because Mock.VerifyAll required all mock file provider's setup to be executed.
There was a problem hiding this comment.
In other tests, where Mock.VerifyAll worked, I used it.
Ok, I'll recheck tests once again.
| context.Verify(c => c.RequestServices.GetService(typeof(IWebHostEnvironment))); | ||
| context.Verify(c => c.RequestServices.GetService(typeof(IMemoryCache))); | ||
| context.Verify(c => c.Response.Headers); | ||
| env.Verify(e => e.WebRootFileProvider); |
There was a problem hiding this comment.
Why not use Mock.VerifyAll?
| context.Verify(c => c.RequestServices.GetService(typeof(IWebHostEnvironment))); | ||
| context.Verify(c => c.RequestServices.GetService(typeof(IMemoryCache))); | ||
| context.Verify(c => c.Response.Headers); | ||
| env.Verify(e => e.WebRootFileProvider); |
There was a problem hiding this comment.
Why not use Mock.VerifyAll?
|
|
||
| Assert.Equal(new[] { "b.css", "a.css" }, files); | ||
|
|
||
| env.Verify(e => e.WebRootFileProvider); |
|
|
||
| Assert.Equal(new[] { "sub/nested.css", "root.css" }, files); | ||
|
|
||
| env.Verify(e => e.WebRootFileProvider); |
| Assert.True(indexStyleA < indexAlpha && indexStyleB < indexAlpha, | ||
| "style-*.css group precedes alpha.css group"); | ||
|
|
||
| env.Verify(e => e.WebRootFileProvider); |
Replaced Moq-based MockFileProvider with a plain implementation.
|
@jzabroski, replaced manual verifies with VerifyAll. |
|
LGTM. Thank you for taking the feedback in so fast. |
ready to merge. |

Update 2026-06-11: I've replaced manual caching with GlobbingUrlBuilder. Plus, I've added two more tests to check that GlobbingUrlBuilder works with nested directories/files.
At the moment,
Asset.ExpandGlobs{:.c#} doesn't cache results ofprovider.GetAllFiles("/"), so in the following example (simplified example - in the real app, this bundle contains different globs at different nested levels):Asset.ExpandGlobswill get all files 6 times. It can be a problem, if the virtual files hierarchy is huge.This PR caches the results of
provider.GetAllFiles("/")perproviderintoHttpContext.Itemsdictionary.Also, this fixes the case when many complex bundles are located on the same page, e.g.:
Each of them will reuse the cached file hierarchy if possible.
P.S. As you can see, I've tested this with
EnableTagHelperBundling = false.ScriptTagHelperandLinkTagHelpercallAsset.ExpandGlobson each execution, which resulted in 5 seconds of rendering for just the script tags in my app, instead of something like ~0.1 seconds. :)