Skip to content

Commit

Permalink
Use System.Text.Json instead of Newton.Json (#525)
Browse files Browse the repository at this point in the history
* Use System.Text.Json instead of Newtonsoft.Json. Approximately 2x faster result output serialization with significantly reduced memory usage (for JSON format).

* Adds simple benchmark for json writer.

---------

Co-authored-by: Gabe Stocco <[email protected]>
  • Loading branch information
dbalikhin and gfs authored Feb 8, 2023
1 parent d39172e commit 5059422
Show file tree
Hide file tree
Showing 40 changed files with 414 additions and 250 deletions.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion>
<RootNamespace>ApplicationInspector.Benchmarks</RootNamespace>
<Company>Microsoft Corporation</Company>
</PropertyGroup>

<ItemGroup>
Expand All @@ -13,6 +15,7 @@

</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AppInspector.CLI\AppInspector.CLI.csproj" />
<ProjectReference Include="..\AppInspector\AppInspector.Commands.csproj" />
<ProjectReference Include="..\AppInspector.Tests\AppInspector.Tests.csproj" />
</ItemGroup>
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ public static void Main(string[] args)
{
// new DebugInProcessConfig()
//BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, new DebugInProcessConfig());
var summary = BenchmarkRunner.Run<AnalyzeBenchmark>();
var summary = BenchmarkRunner.Run<WriterBench>();
}
}
71 changes: 71 additions & 0 deletions AppInspector.Benchmarks/WriterBench.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.IO;
using System.Reflection;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using DotLiquid;
using Microsoft.ApplicationInspector.CLI;
using Microsoft.ApplicationInspector.Commands;
using Microsoft.ApplicationInspector.RulesEngine;

namespace ApplicationInspector.Benchmarks;
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net70)]
public class WriterBench
{
[Params(1000, 10000)]
public int N;

// Holds the result object which will be serialized
private AnalyzeResult _result;

[GlobalSetup]
public void GlobalSetup()
{
var _exerpt = "Hello World";
var helper = new MetaDataHelper("..");
var matchRecord = new MatchRecord("rule-id", "rule-name")
{
Boundary = new Boundary() { Index = 0, Length = 1 },
EndLocationColumn = 0,
EndLocationLine = 1,
Excerpt = _exerpt,
FileName = "TestFile",
LanguageInfo = new LanguageInfo(),
Tags = new []{"TestTag"}
};
for (int i = 0; i < N; i++)
{
helper.AddMatchRecord(matchRecord);
}
helper.PrepareReport();

_result = new AnalyzeResult() { Metadata = helper.Metadata, ResultCode = 0 };
}

[Benchmark(Baseline = true)]
public void ExportRecordsToJson()
{
var tmpPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
CLIAnalyzeCmdOptions analyzeOpts = new CLIAnalyzeCmdOptions()
{
OutputFileFormat = "json",
OutputFilePath = tmpPath
};
var writerFactory = new WriterFactory();
var writer = writerFactory.GetWriter(analyzeOpts);
writer.WriteResults(_result,analyzeOpts);
File.Delete(tmpPath);
}

public static string GetExecutingDirectoryName()
{
if (Assembly.GetEntryAssembly()?.GetName().CodeBase is string codeBaseLoc)
{
var location = new Uri(codeBaseLoc);
return new FileInfo(location.AbsolutePath).Directory?.FullName ?? string.Empty;
}

return string.Empty;
}
}
41 changes: 21 additions & 20 deletions AppInspector.CLI/TagInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using DotLiquid;
using Microsoft.ApplicationInspector.RulesEngine;
using Newtonsoft.Json;

namespace Microsoft.ApplicationInspector.Commands;

Expand All @@ -21,17 +21,18 @@ public enum tagInfoType
allTags
}

[JsonProperty(PropertyName = "type")] public tagInfoType Type;
[JsonPropertyName("type")]
public tagInfoType Type;

public TagCategory()
{
Groups = new List<TagGroup>();
}

[JsonProperty(PropertyName = "categoryName")]
[JsonPropertyName("categoryName")]
public string? Name { get; set; }

[JsonProperty(PropertyName = "groups")]
[JsonPropertyName("groups")]
public List<TagGroup>? Groups { get; set; }
}

Expand All @@ -45,14 +46,14 @@ public TagGroup()
Patterns = new List<TagSearchPattern>();
}

[JsonProperty(PropertyName = "title")] public string? Title { get; set; }
[JsonPropertyName("title")] public string? Title { get; set; }

[JsonIgnore] public string? IconURL { get; set; }

[JsonProperty(PropertyName = "dataRef")]
[JsonPropertyName("dataRef")]
public string? DataRef { get; set; }

[JsonProperty(PropertyName = "patterns")]
[JsonPropertyName("patterns")]
public List<TagSearchPattern>? Patterns { get; set; }
}

Expand All @@ -61,7 +62,7 @@ public class TagSearchPattern : Drop
private Regex? _expression;
private string _searchPattern = "";

[JsonProperty(PropertyName = "searchPattern")]
[JsonPropertyName("searchPattern")]
public string SearchPattern
{
get => _searchPattern;
Expand All @@ -85,19 +86,19 @@ public Regex Expression
}
}

[JsonProperty(PropertyName = "displayName")]
[JsonPropertyName("displayName")]
public string? DisplayName { get; set; }

[JsonProperty(PropertyName = "detectedIcon")]
[JsonPropertyName("detectedIcon")]
public string? DetectedIcon { get; set; } = "fas fa-cat"; //default

[JsonProperty(PropertyName = "notDetectedIcon")]
[JsonPropertyName("notDetectedIcon")]
public string? NotDetectedIcon { get; set; }

[JsonProperty(PropertyName = "detected")]
[JsonPropertyName("detected")]
public bool Detected { get; set; }

[JsonProperty(PropertyName = "details")]
[JsonPropertyName("details")]
public string Details
{
get
Expand All @@ -107,7 +108,7 @@ public string Details
}
}

[JsonProperty(PropertyName = "confidence")]
[JsonPropertyName("confidence")]
public string Confidence { get; set; } = "Medium";

public static bool ShouldSerializeExpression()
Expand All @@ -123,14 +124,14 @@ public class TagInfo : Drop
{
private string _confidence = "Medium";

[JsonProperty(PropertyName = "tag")] public string? Tag { get; set; }
[JsonPropertyName("tag")] public string? Tag { get; set; }

[JsonProperty(PropertyName = "displayName")]
[JsonPropertyName("displayName")]
public string? ShortTag { get; set; }

[JsonIgnore] public string? StatusIcon { get; set; }

[JsonProperty(PropertyName = "confidence")]
[JsonPropertyName("confidence")]
public string Confidence
{
get => _confidence;
Expand All @@ -143,14 +144,14 @@ public string Confidence
}
}

[JsonProperty(PropertyName = "severity")]
[JsonPropertyName("severity")]
public string Severity { get; set; } = "Moderate";

[JsonProperty(PropertyName = "detected")]
[JsonPropertyName("detected")]
public bool Detected { get; set; }
}

public class TagException
{
[JsonProperty(PropertyName = "tag")] public string? Tag { get; set; }
[JsonPropertyName("tag")] public string? Tag { get; set; }
}
37 changes: 27 additions & 10 deletions AppInspector.CLI/Writers/AnalyzeHtmlWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
using Microsoft.ApplicationInspector.RulesEngine;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using System.Text.Json;

namespace Microsoft.ApplicationInspector.CLI;

Expand All @@ -27,7 +28,7 @@ public class AnalyzeHtmlWriter : CommandResultsWriter

private MetaData? _appMetaData;

public AnalyzeHtmlWriter(TextWriter textWriter, ILoggerFactory? loggerFactory = null) : base(textWriter)
public AnalyzeHtmlWriter(StreamWriter textWriter, ILoggerFactory? loggerFactory = null) : base(textWriter)
{
_logger = loggerFactory?.CreateLogger<AnalyzeHtmlWriter>() ?? NullLogger<AnalyzeHtmlWriter>.Instance;
KeyedTagInfoLists = new Dictionary<string, List<TagInfo>>();
Expand Down Expand Up @@ -91,7 +92,7 @@ private void WriteHtmlResult()
string? jsonData;
try
{
jsonData = JsonConvert.SerializeObject(data);
jsonData = JsonSerializer.Serialize(data);
}
catch (Exception e)
{
Expand Down Expand Up @@ -232,9 +233,23 @@ public void PopulateTagGroups()
//read default/user preferences on what tags to report presence on and groupings
if (File.Exists(Utils.GetPath(Utils.AppPath.tagGroupPref)))
{
TagGroupPreferences =
JsonConvert.DeserializeObject<List<TagCategory>>(
File.ReadAllText(Utils.GetPath(Utils.AppPath.tagGroupPref)));
try
{
var options = new JsonSerializerOptions
{
ReadCommentHandling = JsonCommentHandling.Skip, // Allow strings to start with '/', e.g., "// Copyright (C) Microsoft. All rights reserved"
};
TagGroupPreferences =
JsonSerializer.Deserialize<List<TagCategory>>(
File.ReadAllText(Utils.GetPath(Utils.AppPath.tagGroupPref)), options);
}
catch (Exception e)
{
_logger.LogError(
"Failed to populate tag groups. Failed to serialize JSON representation of results in memory. {Type} : {Message}",
e.GetType().Name, e.Message);
throw;
}
}
else
{
Expand Down Expand Up @@ -627,13 +642,15 @@ public List<TagCounterUI> ConvertTagCounters(IEnumerable<MetricTagCounter> metri
/// </summary>
public class TagCounterUI : Drop
{
[JsonProperty(PropertyName = "tag")] public string? Tag { get; set; }
[JsonPropertyName("tag")]
public string? Tag { get; set; }

[JsonProperty(PropertyName = "displayName")]
[JsonPropertyName("displayName")]
public string? ShortTag { get; set; }

[JsonProperty(PropertyName = "count")] public int Count { get; set; }
[JsonPropertyName("count")]
public int Count { get; set; }

[JsonProperty(PropertyName = "includeAsMatch")]
[JsonPropertyName("includeAsMatch")]
public bool IncludeAsMatch => false;
}
30 changes: 23 additions & 7 deletions AppInspector.CLI/Writers/AnalyzeJsonWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
using Microsoft.ApplicationInspector.Commands;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using System.Text.Json;
using System;

namespace Microsoft.ApplicationInspector.CLI;

Expand All @@ -20,20 +22,33 @@ public class AnalyzeJsonWriter : CommandResultsWriter
{
private readonly ILogger<AnalyzeJsonWriter> _logger;

public AnalyzeJsonWriter(TextWriter textWriter, ILoggerFactory? loggerFactory = null) : base(textWriter)
public AnalyzeJsonWriter(StreamWriter textWriter, ILoggerFactory? loggerFactory = null) : base(textWriter)
{
_logger = loggerFactory?.CreateLogger<AnalyzeJsonWriter>() ?? NullLogger<AnalyzeJsonWriter>.Instance;
}

public override void WriteResults(Result result, CLICommandOptions commandOptions, bool autoClose = true)
{
var analyzeResult = (AnalyzeResult)result;
if (StreamWriter == null)
{
throw new ArgumentNullException(nameof(StreamWriter));
}

JsonSerializer jsonSerializer = new();
jsonSerializer.Formatting = Formatting.Indented;
if (TextWriter != null)
try
{
var options = new JsonSerializerOptions
{
WriteIndented = true,
};
JsonSerializer.Serialize(StreamWriter.BaseStream, analyzeResult, options);
}
catch (Exception e)
{
jsonSerializer.Serialize(TextWriter, analyzeResult);
_logger.LogError(
"Failed to serialize JSON representation of results in memory. {Type} : {Message}",
e.GetType().Name, e.Message);
throw;
}

if (autoClose)
Expand All @@ -47,6 +62,7 @@ public override void WriteResults(Result result, CLICommandOptions commandOption
/// </summary>
private class TagsFile
{
[JsonProperty(PropertyName = "tags")] public string[]? Tags { get; set; }
[JsonPropertyName("tags")]
public string[]? Tags { get; set; }
}
}
Loading

0 comments on commit 5059422

Please sign in to comment.