65.9K
CodeProject 正在变化。 阅读更多。
Home

使用动态代理延迟反序列化大型 JSON 文件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2018年7月26日

MIT

5分钟阅读

viewsIcon

22337

downloadIcon

105

用于延迟反序列化大型 JSON 文件的动态代理

引言

本文中描述的解决方案是为了解决我在标准音乐字体布局(https://www.smufl.org/)的控件库项目 Manufaktura.Controlshttps://codeproject.org.cn/Articles/1252423/Music-Notation-in-NET)实现过程中遇到的一个问题而发明的。

标准音乐字体布局(简称 SMuFL)是由 Steinberg 发起,目前由 W3C 音乐符号社区组开发的字体标准。符合 SMuFL 规范的音乐字体会附带一个 JSON 文件,其中包含描述默认雕刻设置(如小节线粗细、梁的粗细等)以及字形之间关系的元数据,例如字形大小、字形切口(用于将字形紧密排列在一起)等。SMuFL 的完整规范可以在这里找到:http://w3c.github.io/smufl/gitbook/specification/

所有这些设置都存储在一个包含数千个节点的 JSON 文件中。单独的类 Manufaktura.Controls.Model.SMuFL.GlyphBBoxes,它只映射了 JSON 文件的一小部分,就包含 2964 个属性。根据我的测量,使用流行的框架 Newtonsoft.Json 反序列化整个元数据文件在我机器上(我使用的是第 7 代 i7 CPU)需要 4.8 秒。其他用户报告说,在某些机器上可能需要长达 8 秒。

JSON 元数据文件中包含的大部分数据对大多数应用程序来说是不必要的。如果你熟悉音乐,你可能知道大多数乐谱不使用微音调调整,而 SMuFL 规范提供了来自几种微音调系统的字符。你可能也不需要诸如 PictJingleBellsPictMusicalSawPeinkoferWindRimOnly 这样的字形,不管它们是什么意思……

显而易见的解决方案是只读取 JSON 文件的一部分,但是将 JSON 树结构映射到强类型对象会很诱人,这样你就可以在不需要手动遍历 JSON 树的情况下访问所有需要的属性。注意在这个方法中它看起来有多简单(其中 ISMuFLFontMetadata 是一个反序列化的 JSON 文件)

[Units(Units.Linespaces)]
public BoundingBox GetSMuFLBoundingBox(ISMuFLFontMetadata metadata)
{
    if (RepeatSign == RepeatSignType.Backward) return metadata.GlyphBBoxes.RepeatRight;
    else if (RepeatSign == RepeatSignType.Forward) return metadata.GlyphBBoxes.RepeatLeft;
    else return null;
}

既能部分读取 JSON 又能保持强类型对象简洁性的方法是实现一个动态代理。

它是如何工作的

动态代理有很多实现,但我想让我的代码支持 .NET Standard,所以我决定使用 Microsoft 的官方 System.Reflection.DispatchProxy。动态代理基本上会在运行时动态创建一个类型。这个生成的类型会包装另一个类型并添加一些额外的逻辑。例如,它可以重写 virtual 方法,并在用于测量性能、处理异常等的块中包装它们。这种技术是方面编程方法论的一部分。

DispatchProxy 是一个简单的代理,它实现给定的接口,并允许程序员在 Invoke 方法中提供实现,每当调用接口的任何方法时都会调用该方法。

protected override object Invoke(MethodInfo targetMethod, object[] args)

这是我们映射 JSON 节点到对象的接口

public interface ISMuFLFontMetadata
{
    [JsonProperty("fontName")]
    string FontName { get; set; }

    [JsonProperty("fontVersion")]
    double FontVersion { get; set; }

    [JsonProperty("engravingDefaults")]
    Dictionary<string, double> EngravingDefaults { get; set; }

    [JsonProperty("glyphBBoxes")]
    IGlyphBBoxes GlyphBBoxes { get; set; }

    [JsonProperty("glyphsWithAlternates")]
    Dictionary<string, GlyphsWithAlternate> GlyphsWithAlternates { get; set; }

    [JsonProperty("glyphsWithAnchors")]
    GlyphsWithAnchors GlyphsWithAnchors { get; set; }

    [JsonProperty("ligatures")]
    Dictionary<string, Ligature> Ligatures { get; set; }

    [JsonProperty("optionalGlyphs")]
    OptionalGlyphs OptionalGlyphs { get; set; }

    [JsonProperty("sets")]
    Dictionary<string, GlyphSet> Sets { get; set; }
}

我们的目标是在访问每个 JSON 属性时反序列化它。如果一个属性不被使用,它将根本不会被反序列化。这是示例实现

public abstract class LazyLoadJsonProxy : DispatchProxy
{
        public static object Create(Type interfaceType, string json)
        {
            var proxyType = typeof(LazyLoadJsonProxy<>).MakeGenericType(interfaceType);
            var method = proxyType.GetTypeInfo().GetDeclaredMethods
            (nameof(Create)).First(m => m.GetParameters().First().ParameterType == typeof(string));
            return method.Invoke(null, new object[] { json });
        }
}

public class LazyLoadJsonProxy<TInterface> : LazyLoadJsonProxy
    {
        private ConcurrentDictionary<string, object> 
                                cache = new ConcurrentDictionary<string, object>();
        private string jsonString;

        public static TInterface Create(string json)
        {
            var proxy = Create<TInterface, LazyLoadJsonProxy<TInterface>>() 
                                                     as LazyLoadJsonProxy<TInterface>;
            proxy.jsonString = json;
            return (TInterface)(object)proxy;
        }

        protected override object Invoke(MethodInfo targetMethod, object[] args)
        {
            if (cache.ContainsKey(targetMethod.Name)) return cache[targetMethod.Name];

               var jsonPropertyAttribute = targetMethod.DeclaringType
                    .GetTypeInfo()
                    .GetDeclaredProperty(targetMethod.Name.Replace("get_", ""))?
                    .GetCustomAttribute<JsonPropertyAttribute>();

                if (jsonPropertyAttribute == null) return TryAddDefaultValue
                                      (targetMethod.Name, targetMethod.ReturnType);

                using (var textReader = new StringReader(jsonString))
                using (var reader = new JsonTextReader(textReader))
                {
                    while (reader.Read())
                    {
                        if (reader.Path != jsonPropertyAttribute.PropertyName) continue;

                        var token = JToken.Load(reader);
                        if (targetMethod.ReturnType.GetTypeInfo().IsInterface)
                        {
                            var prop = token as JProperty;
                            if (prop != null)
                            {
                                var proxy = Create(targetMethod.ReturnType, prop.Value.ToString());
                                return TryAddValue(targetMethod.Name, proxy);
                            }
                            else
                            {
                               var proxy = Create(targetMethod.ReturnType, token.ToString());
                                 return TryAddValue(targetMethod.Name, proxy);
                            }
                        }

                        var property = token as JProperty;
                        if (property != null)
                            return TryAddValue(targetMethod.Name, 
                                   property.Value.ToObject(targetMethod.ReturnType));
                        else
                            return TryAddValue(targetMethod.Name, 
                                               token.ToObject(targetMethod.ReturnType));
                     }
                }

                return TryAddDefaultValue(targetMethod.Name, targetMethod.ReturnType);
        }

        private object TryAddDefaultValue(string name, Type type)
        {
            var value = type.GetDefaultValue();
            return TryAddValue(name, value);
        }

        private object TryAddValue(string name, object value)
        {
            cache.TryAdd(name, value);
            return value;
        }
    }

本文附带的代码包含性能测量,但为了清晰起见,我已将其从本示例中删除。

每次访问属性时,JSON 序列化器只会反序列化与该属性关联的 JSON 文件部分。有些属性包含大对象,例如 GlyphBBoxes 包含 2000 多个节点。反序列化这样的属性非常耗时,所以我决定实现代理嵌套。如果属性类型是接口,则会创建一个新的代理,而不是一次性反序列化整个子树。

如何使用

这是老式反序列化 JSON 的方法(一次性全部反序列化)

var metadata = JsonConvert.DeserializeObject<SMuFLFontMetadata>(jsonString);

这是使用代理的方法

var metadata = LazyLoadJsonProxy<ISMuFLFontMetadata>.Create(jsonString);

在这两个示例中,元数据的类型都是 ISMuFLFontMetadata,但实现不同。在第一个示例中,我们有一个正常的类 SMuFLFontMetadata(用自动属性实现),在第二个示例中,我们有我们的代理对象。

测试

我创建了一些单元测试来衡量这种方法的性能

[TestMethod]
public void JsonDeserialziationTestWithoutProxy()
{
    var assembly = typeof(SerializationTests).Assembly;
    var resourceName = $"{typeof(SerializationTests).Namespace}.Assets.bravura_metadata.json";

    using (var stream = assembly.GetManifestResourceStream(resourceName))
    using (var reader = new StreamReader(stream))
    {
        string result = reader.ReadToEnd();
        var sw = new Stopwatch();
        sw.Start();
        var traditionallyLoadedMetadata = JsonConvert.DeserializeObject<SMuFLFontMetadata>(result);
        sw.Stop();

        Debug.WriteLine(sw.Elapsed);
    }
}

[TestMethod]
public void JsonDeserializationTestWithProxy()
{
    var assembly = typeof(SerializationTests).Assembly;
    var resourceName = $"{typeof(SerializationTests).Namespace}.Assets.bravura_metadata.json";

    using (var stream = assembly.GetManifestResourceStream(resourceName))
    using (var reader = new StreamReader(stream))
    {
        string result = reader.ReadToEnd();
        var metadata = LazyLoadJsonProxy<ISMuFLFontMetadata>.Create(result);
        var defaults = metadata.EngravingDefaults;
            
        var bboxes = metadata.GlyphBBoxes;
        var prop1 = bboxes.AccdnCombDot;
        var prop2 = bboxes.WindTightEmbouchure;
        var prop3 = bboxes.WindRimOnly;
        var prop4 = bboxes.MensuralLongaVoidStemDownRight;
        var prop5 = bboxes.AccSagittalFlat11LDown;
        var prop6 = bboxes.NoteheadSquareBlackWhite;
        var prop7 = bboxes.NoteheadWholeWithX;
        var prop8 = bboxes.ElecMixingConsole;
        var prop9 = bboxes.ElecPause;
        var prop10 = bboxes.PictBeaterWoodTimpaniUp;
        var prop11 = bboxes.AccdnCombLh2RanksEmpty;
        var prop12 = bboxes.AccSagittalSharp5V13LUp;
        var prop13 = bboxes.MensuralNoteheadLongaWhite;
        var prop14 = bboxes.OrnamentTrill;
        var prop15 = bboxes.OrnamentTremblementCouperin;
        var prop16 = bboxes.AccSagittalSharp19SUp;
        var prop17 = bboxes.NoteShapeRoundDoubleWhole;
        var prop18 = bboxes.WindWeakAirPressure;
        var prop19 = bboxes.WindRelaxedEmbouchure;
        var prop20 = bboxes.AccdnCombLh2RanksEmpty;

        var metadataAsProxy = (LazyLoadJsonProxy)metadata;
        var bboxesAsProxy = (LazyLoadJsonProxy)bboxes;
        var elapsedWithProxy = metadataAsProxy.TotalTimeSpentOnDeserialization + 
                               bboxesAsProxy.TotalTimeSpentOnDeserialization;

        Debug.WriteLine(elapsedWithProxy);
    }
}

第一个示例测量一次性反序列化整个 JSON 时的性能。第二个示例创建一个代理对象并访问一些随机属性。Manufaktura.Controls 实际上使用的属性不超过几十个,所以我决定随机选择 20 个。

在实际情况中,同时访问 20 个属性的可能性非常小。我创建了一个测试来检查实际渲染中的性能

[TestMethod]
public void JsonDeserializationTestWithProxyOnRealExample()
{
    var assembly = typeof(SerializationTests).Assembly;
    var resourceName = 
      $"{typeof(SerializationTests).Namespace}.Assets.bravura_metadata.json";
    var scoreResourceName = 
      $"{typeof(SerializationTests).Namespace}.Assets.JohannChristophBachFull3.0.xml";

    using (var stream = assembly.GetManifestResourceStream(resourceName))
    using (var scoreStream = assembly.GetManifestResourceStream(scoreResourceName))
    using (var reader = new StreamReader(stream))
    using (var scoreReader = new StreamReader(scoreStream))
    {
        var sw = new Stopwatch();
        sw.Start();

        string metadataJson = reader.ReadToEnd();
        var metadata = LazyLoadJsonProxy<ISMuFLFontMetadata>.Create(metadataJson);

        var scoreString = scoreReader.ReadToEnd();
        var settings = new HtmlScoreRendererSettings();
        settings.RenderSurface = HtmlScoreRendererSettings.HtmlRenderSurface.Svg;
        settings.LoadSMuFLFont(metadata, "Bravura", 24, "/fakeuri");
        settings.Scale = 1;
        settings.CustomElementPositionRatio = 0.8;
        settings.IgnorePageMargins = true;

        var renderer = 
           new HtmlSvgScoreRenderer(new XElement("root"), "testCanvas", settings);
        renderer.Render(scoreString.ToScore());

        sw.Stop();
    
        var metadataAsProxy = (LazyLoadJsonProxy)metadata;

        Debug.WriteLine($"All rendering done in {sw.Elapsed}");
  
        var deserTime = metadataAsProxy.GetTotalDeserializationTimeWithChildElements();

        Debug.WriteLine($"Deserialization done in {deserTime}");
    }
}

结果

这些是测试结果(以秒为单位)

 

不使用代理
(一次性
反序列化
整个 JSON)

启用代理后
第一层

使用嵌套代理

使用嵌套代理
在真实示例上

测量 1

4,97

3,93

1,68

1,51

测量 2

4,71

4,35

1,68

1,51

测量 3

4,71

4,00

1,69

1,49

Average

4,80

4,10

1,68

1,50

此外,我的机器上测试 WPF 应用程序 Manufaktura.Controls.WPF.Test 的启动时间(附加调试器时)已从 12 秒减少到 5 秒。

结论

提出的方法在性能上提供了显著的改进,并允许程序员继续使用强类型模型。特定节点的反序列化被推迟到请求时才进行,这可能导致一些不可预测性。为了确保稳定的性能,程序员必须了解 JSON 文件的结构,并决定哪些子树将被一次性反序列化,哪些将被代理进行惰性反序列化。

您还必须牢记,如果您正在调试应用程序并尝试检查代理的内容,整个 JSON 将一次性被反序列化,因为所有属性都将被解析。这可能会在调试时导致一些性能问题。

© . All rights reserved.