[CmdPal] Fix slow fuzzy search in apps extension by properly handling null-terminated strings from SHLoadIndirectString (#40198)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This change addresses a significant performance regression caused by
improper handling of null-terminated strings returned from the
SHLoadIndirectString API.

Previously, the output buffer was converted to string using
Span<char>.ToString() without trimming at the null terminator (\0). As a
result, the entire buffer (1024 characters) was converted, including
trailing garbage data after the valid string.

This caused the fuzzy matching logic to process unnecessarily long
strings, leading to excessive CPU usage and input lag (~2 seconds delay
per keystroke).

The fix properly locates the first null terminator in the buffer and
slices the span before converting to string, eliminating trailing
garbage characters. This reduces the workload in the scoring function
and resolves the input lag in the apps extension search.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] **Closes:** #40197
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
leileizhang 2025-06-24 13:41:53 +08:00 committed by Gordon Lam (SH)
parent 72f7ffcbc1
commit e03ce8a7b6

View File

@ -173,6 +173,25 @@ public class UWPApplication : IProgram
return false;
}
private static string TryLoadIndirectString(string source, Span<char> buffer, string errorContext)
{
try
{
PInvoke.SHLoadIndirectString(source, buffer).ThrowOnFailure();
var len = buffer.IndexOf('\0');
var loaded = len >= 0
? buffer[..len].ToString()
: buffer.ToString();
return string.IsNullOrEmpty(loaded) ? string.Empty : loaded;
}
catch (Exception ex)
{
Logger.LogError($"Unable to load resource {source} : {errorContext} : {ex.Message}");
return string.Empty;
}
}
internal unsafe string ResourceFromPri(string packageFullName, string resourceReference)
{
const string prefix = "ms-resource:";
@ -221,28 +240,15 @@ public class UWPApplication : IProgram
Span<char> outBuffer = stackalloc char[1024];
var source = $"@{{{packageFullName}? {parsed}}}";
try
{
PInvoke.SHLoadIndirectString(source, outBuffer).ThrowOnFailure();
var loaded = TryLoadIndirectString(source, outBuffer, resourceReference);
var loaded = outBuffer.ToString();
return string.IsNullOrEmpty(loaded) ? string.Empty : loaded;
}
catch (Exception)
if (!string.IsNullOrEmpty(loaded))
{
try
{
var sourceFallback = $"@{{{packageFullName}?{parsedFallback}}}";
PInvoke.SHLoadIndirectString(sourceFallback, outBuffer).ThrowOnFailure();
var loaded = outBuffer.ToString();
return string.IsNullOrEmpty(loaded) ? string.Empty : loaded;
}
catch (Exception)
{
// ProgramLogger.Exception($"Unable to load resource {resourceReference} from {packageFullName}", new InvalidOperationException(), GetType(), packageFullName);
return string.Empty;
}
return loaded;
}
var sourceFallback = $"@{{{packageFullName}?{parsedFallback}}}";
return TryLoadIndirectString(sourceFallback, outBuffer, $"{resourceReference} (fallback)");
}
else
{