Skip to content

Commit dd0e25c

Browse files
committed
Windows: Fix unbounded growth of scoped attributes stored in PlayerPrefs/registry.
Fix unbounded growth of scoped attributes stored in PlayerPrefs/registry on Windows. - Maintain a HashSet of keys and serialize back to a compact List to prevent duplicates - Skip PlayerPrefs writes when the new value matches the stored value - Parse + cleanup in CleanScopedAttributes/GetScopedAttributes
1 parent 0905d45 commit dd0e25c

File tree

1 file changed

+148
-20
lines changed

1 file changed

+148
-20
lines changed

Runtime/Native/Windows/NativeClient.cs

Lines changed: 148 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ private string GetPluginDirectoryPath()
333333
const string pluginDir = "Plugins";
334334
return Path.Combine(Application.dataPath, pluginDir);
335335
}
336+
336337
/// <summary>
337338
/// Generate path to Crashpad handler binary
338339
/// </summary>
@@ -367,12 +368,27 @@ internal static void CleanScopedAttributes()
367368
{
368369
return;
369370
}
370-
var attributes = JsonUtility.FromJson<ScopedAttributesContainer>(attributesJson);
371-
foreach (var attributeKey in attributes.Keys)
371+
372+
try
373+
{
374+
var attributes = JsonUtility.FromJson<ScopedAttributesContainer>(attributesJson);
375+
if (attributes?.Keys != null)
376+
{
377+
foreach (var attributeKey in attributes.Keys)
378+
{
379+
PlayerPrefs.DeleteKey(string.Format(ScopedAttributesPattern, attributeKey));
380+
}
381+
}
382+
}
383+
catch (Exception e)
372384
{
373-
PlayerPrefs.DeleteKey(string.Format(ScopedAttributesPattern, attributeKey));
385+
Debug.LogWarning($"Backtrace Failed to parse scoped attributes for cleanup: {e.Message}");
386+
}
387+
finally
388+
{
389+
PlayerPrefs.DeleteKey(ScopedAttributeListKey);
390+
PlayerPrefs.Save();
374391
}
375-
PlayerPrefs.DeleteKey(ScopedAttributeListKey);
376392
}
377393

378394
internal static IDictionary<string, string> GetScopedAttributes()
@@ -382,8 +398,25 @@ internal static IDictionary<string, string> GetScopedAttributes()
382398
{
383399
return new Dictionary<string, string>();
384400
}
385-
var result = new Dictionary<string, string>();
386-
var attributes = JsonUtility.FromJson<ScopedAttributesContainer>(attributesJson);
401+
402+
var result = new Dictionary<string, string>(StringComparer.Ordinal);
403+
ScopedAttributesContainer attributes = null;
404+
405+
try
406+
{
407+
attributes = JsonUtility.FromJson<ScopedAttributesContainer>(attributesJson);
408+
}
409+
catch (Exception e)
410+
{
411+
Debug.LogWarning($"Backtrace Failed to parse scoped attributes at read: {e.Message}");
412+
return result;
413+
}
414+
415+
if (attributes?.Keys == null)
416+
{
417+
return result;
418+
}
419+
387420
foreach (var attributeKey in attributes.Keys)
388421
{
389422
var value = PlayerPrefs.GetString(string.Format(ScopedAttributesPattern, attributeKey), string.Empty);
@@ -411,7 +444,7 @@ internal static IDictionary<string, string> GetScopedAttributes()
411444
}
412445

413446
/// <summary>
414-
/// Adds attributes to scoped registry and to native clietn
447+
/// Adds attributes to scoped registry and to native client
415448
/// </summary>
416449
/// <param name="key">attribute key</param>
417450
/// <param name="value">attribute value</param>
@@ -424,23 +457,103 @@ private void AddAttributes(string key, string value)
424457
AddScopedAttribute(key, value);
425458
}
426459

460+
/// <summary>Load a deduped set of scoped attribute keys from PlayerPrefs.</summary>
461+
private HashSet<string> LoadScopedKeys()
462+
{
463+
var keys = new HashSet<string>(StringComparer.Ordinal);
464+
var attributesJson = PlayerPrefs.GetString(ScopedAttributeListKey);
465+
if (HasScopedAttributesEmpty(attributesJson))
466+
{
467+
try
468+
{
469+
var container = JsonUtility.FromJson<ScopedAttributesContainer>(attributesJson);
470+
if (container?.Keys != null && container.Keys.Count > 0)
471+
{
472+
foreach (var k in container.Keys)
473+
{
474+
if (!string.IsNullOrEmpty(k))
475+
{
476+
keys.Add(k);
477+
}
478+
}
479+
}
480+
}
481+
catch (Exception e)
482+
{
483+
Debug.LogWarning($"Backtrace Failed to parse scoped attributes list. {e.Message}");
484+
keys.Clear();
485+
}
486+
}
487+
return keys;
488+
}
489+
490+
/// <summary>Persist a deduped list of keys back to PlayerPrefs.</summary>
491+
private void SaveScopedKeys(HashSet<string> keys)
492+
{
493+
var container = new ScopedAttributesContainer
494+
{
495+
Keys = keys.OrderBy(k => k, StringComparer.Ordinal).ToList()
496+
};
497+
PlayerPrefs.SetString(ScopedAttributeListKey, JsonUtility.ToJson(container));
498+
PlayerPrefs.Save();
499+
}
500+
501+
/// <summary>Return the currently stored "value" for a scoped key or "null" if missing.</summary>
502+
private string GetScopedValue(string key)
503+
{
504+
var prefsKey = string.Format(ScopedAttributesPattern, key);
505+
return PlayerPrefs.HasKey(prefsKey) ? PlayerPrefs.GetString(prefsKey) : null;
506+
}
507+
508+
/// <summary>Set the stored value for a scoped key.</summary>
509+
private void SetScopedValue(string key, string value)
510+
{
511+
PlayerPrefs.SetString(string.Format(ScopedAttributesPattern, key), value ?? string.Empty);
512+
}
513+
427514
/// <summary>
428515
/// Adds dictionary of attributes to player prefs for windows crashes captured by unity crash handler
429516
/// </summary>
430-
/// <param name="atributes">Attributes</param>
517+
/// <param name="attributes">Attributes</param>
431518
internal void AddScopedAttributes(IDictionary<string, string> attributes)
432519
{
433520
if (!_configuration.SendUnhandledGameCrashesOnGameStartup)
434521
{
435522
return;
436523
}
437-
var attributesContainer = new ScopedAttributesContainer();
438-
foreach (var attribute in attributes)
524+
if (attributes == null || attributes.Count == 0)
439525
{
440-
attributesContainer.Keys.Add(attribute.Key);
441-
PlayerPrefs.SetString(string.Format(ScopedAttributesPattern, attribute.Key), attribute.Value);
526+
return;
442527
}
443-
PlayerPrefs.SetString(ScopedAttributeListKey, JsonUtility.ToJson(attributesContainer));
528+
529+
var keys = LoadScopedKeys();
530+
bool listChanged = false;
531+
532+
foreach (var kv in attributes)
533+
{
534+
var key = kv.Key;
535+
var value = kv.Value ?? string.Empty;
536+
537+
// skip when unchanged
538+
var current = GetScopedValue(key);
539+
if (current == null || !string.Equals(current, value, StringComparison.Ordinal))
540+
{
541+
SetScopedValue(key, value);
542+
}
543+
544+
// deduplication
545+
if (keys.Add(key))
546+
{
547+
listChanged = true;
548+
}
549+
}
550+
551+
if (listChanged)
552+
{
553+
SaveScopedKeys(keys);
554+
}
555+
556+
PlayerPrefs.Save();
444557
}
445558

446559
/// <summary>
@@ -454,14 +567,29 @@ private void AddScopedAttribute(string key, string value)
454567
{
455568
return;
456569
}
457-
var attributesJson = PlayerPrefs.GetString(ScopedAttributeListKey);
458-
var attributes = HasScopedAttributesEmpty(attributesJson)
459-
? JsonUtility.FromJson<ScopedAttributesContainer>(attributesJson)
460-
: new ScopedAttributesContainer();
570+
if (string.IsNullOrEmpty(key))
571+
{
572+
return;
573+
}
574+
575+
var normalized = value ?? string.Empty;
576+
577+
// skip when unchanged
578+
var current = GetScopedValue(key);
579+
if (current != null && string.Equals(current, normalized, StringComparison.Ordinal))
580+
{
581+
return;
582+
}
583+
584+
SetScopedValue(key, normalized);
585+
586+
var keys = LoadScopedKeys();
587+
if (keys.Add(key))
588+
{
589+
SaveScopedKeys(keys);
590+
}
461591

462-
attributes.Keys.Add(key);
463-
PlayerPrefs.SetString(ScopedAttributeListKey, JsonUtility.ToJson(attributes));
464-
PlayerPrefs.SetString(string.Format(ScopedAttributesPattern, key), value);
592+
PlayerPrefs.Save();
465593
}
466594

467595
private static bool HasScopedAttributesEmpty(string attributesJson)

0 commit comments

Comments
 (0)