Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize model serialization by using SharedObjects for reusable data #1090

Merged
merged 4 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Elements/src/Element.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ protected virtual void RaisePropertyChanged([System.Runtime.CompilerServices.Cal
[JsonProperty("Mappings", Required = Required.Default, NullValueHandling = NullValueHandling.Ignore)]
internal Dictionary<string, MappingBase> Mappings { get; set; } = null;

/// <summary>
/// An optional shared object that can be used to share data between elements.
/// </summary>
/// <value></value>
[JsonProperty("SharedObject", Required = Required.Default, NullValueHandling = NullValueHandling.Ignore)]
public SharedObject SharedObject { get; set; }

/// <summary>
/// The method used to set a mapping for a given context.
/// </summary>
Expand Down
119 changes: 88 additions & 31 deletions Elements/src/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
using Elements.Geometry.Solids;
using Elements.GeoJSON;
using System.IO;
using System.Text;

namespace Elements
{
Expand All @@ -21,6 +20,45 @@ namespace Elements
/// </summary>
public class Model
{
private class GatherSubElementsResult
{
/// <summary>
/// List of elements collected from the object.
/// </summary>
public List<Element> Elements { get; } = new List<Element>();
/// <summary>
/// List of shared objects collected from the object.
/// </summary>
public List<SharedObject> SharedObjects { get; } = new List<SharedObject>();
/// <summary>
/// List of elements collected from the shared object's properties.
///
/// If shared object is marked as JsonIgnore (e.g. RepresentationInstance), it will not be
/// serialized to JSON, but its properties will be collected here so they can be used
/// during gltf serialization.
/// </summary>
public List<Element> ElementsFromSharedObjectProperties { get; } = new List<Element>();

public void MergeSubResult(GatherSubElementsResult gatherResult, bool hasJsonIgnore, bool isTypeRelatedToSharedObjects)
{
if (isTypeRelatedToSharedObjects)
{
ElementsFromSharedObjectProperties.AddRange(gatherResult.ElementsFromSharedObjectProperties);
}
else
{
Elements.AddRange(gatherResult.Elements);
}
// do not save shared objects marked with JsonIgnore
if (!hasJsonIgnore)
{
SharedObjects.AddRange(gatherResult.SharedObjects);
Elements.AddRange(gatherResult.ElementsFromSharedObjectProperties);
}
ElementsFromSharedObjectProperties.AddRange(gatherResult.ElementsFromSharedObjectProperties);
}
}

/// <summary>The origin of the model.</summary>
[JsonProperty("Origin", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
[Obsolete("Use Transform instead.")]
Expand All @@ -35,6 +73,10 @@ public class Model
[System.ComponentModel.DataAnnotations.Required]
public System.Collections.Generic.IDictionary<Guid, Element> Elements { get; set; } = new System.Collections.Generic.Dictionary<Guid, Element>();

/// <summary>A collection of SharedObjects keyed by their identifiers.</summary>
[JsonProperty("SharedObjects", Required = Required.Default)]
public System.Collections.Generic.IDictionary<Guid, SharedObject> SharedObjects { get; set; } = new System.Collections.Generic.Dictionary<Guid, SharedObject>();

/// <summary>
/// Collection of subelements from shared objects or RepresentationInstances (e.g. SolidRepresentation.Profile or RepresentationInstance.Material).
/// We do not serialize shared objects to json, but we do include them in other formats like gltf.
Expand Down Expand Up @@ -123,8 +165,8 @@ public void AddElement(Element element, bool gatherSubElements = true, bool upda
// to the elements dictionary first. This will ensure that
// those elements will be read out and be available before
// an attempt is made to deserialize the element itself.
var subElements = RecursiveGatherSubElements(element, out var elementsToIgnore);
foreach (var e in subElements)
var gatherSubElementsResult = RecursiveGatherSubElements(element);
foreach (var e in gatherSubElementsResult.Elements)
{
if (!this.Elements.ContainsKey(e.Id))
{
Expand All @@ -138,7 +180,15 @@ public void AddElement(Element element, bool gatherSubElements = true, bool upda
}
}

foreach (var e in elementsToIgnore)
foreach (var sharedObject in gatherSubElementsResult.SharedObjects)
{
if (!SharedObjects.ContainsKey(sharedObject.Id))
{
SharedObjects.Add(sharedObject.Id, sharedObject);
}
}

foreach (var e in gatherSubElementsResult.ElementsFromSharedObjectProperties)
{
if (!SubElementsFromSharedObjects.ContainsKey(e.Id))
{
Expand Down Expand Up @@ -453,23 +503,21 @@ public static Model FromJson(string json, bool forceTypeReload = false)
return FromJson(json, out _, forceTypeReload);
}

private List<Element> RecursiveGatherSubElements(object obj, out List<Element> elementsToIgnore)
private GatherSubElementsResult RecursiveGatherSubElements(object obj)
{
// A dictionary created for the purpose of caching properties
// that we need to recurse, for types that we've seen before.
var props = new Dictionary<Type, List<PropertyInfo>>();

return RecursiveGatherSubElementsInternal(obj, props, out elementsToIgnore);
return RecursiveGatherSubElementsInternal(obj, props);
}

private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<Type, List<PropertyInfo>> properties, out List<Element> elementsToIgnore)
private GatherSubElementsResult RecursiveGatherSubElementsInternal(object obj, Dictionary<Type, List<PropertyInfo>> properties)
{
var elements = new List<Element>();
elementsToIgnore = new List<Element>();
GatherSubElementsResult result = new GatherSubElementsResult();

if (obj == null)
{
return elements;
return result;
}

var e = obj as Element;
Expand All @@ -478,7 +526,7 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
// Do nothing. The Element has already
// been added. This assumes that that the sub-elements
// have been added as well and we don't need to continue.
return elements;
return result;
}

// This explicit loop is because we have mappings marked as internal so it's elements won't be automatically serialized.
Expand All @@ -487,7 +535,17 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
foreach (var map in e.Mappings ?? new Dictionary<string, MappingBase>())
{
if (!Elements.ContainsKey(map.Value.Id))
{ elements.Add(map.Value); }
{ result.Elements.Add(map.Value); }
}
}

var sharedObject = obj as SharedObject;
// if this shared object is already in the list, we don't need to process and add it again
if (sharedObject != null)
{
if (SharedObjects.ContainsKey(sharedObject.Id))
{
return result;
}
}

Expand All @@ -498,7 +556,7 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
// could be elements.
if (!t.IsClass || t == typeof(string))
{
return elements;
return result;
}

List<PropertyInfo> constrainedProps;
Expand All @@ -515,7 +573,7 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
properties.Add(t, constrainedProps);
}

var elementsFromProperties = new List<Element>();
bool isTypeRelatedToSharedObjects = IsTypeRelatedToSharedObjects(t);
foreach (var p in constrainedProps)
{
try
Expand All @@ -526,12 +584,15 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
continue;
}

// Do not save shared object to the model if it is marked with JsonIgnore (e.g. ElementRepresentation)
bool hasJsonIgnore = p.GetCustomAttributes(typeof(JsonIgnoreAttribute), true).Any();

if (pValue is IList elems)
{
foreach (var item in elems)
{
elementsFromProperties.AddRange(RecursiveGatherSubElementsInternal(item, properties, out var elementsFromItemToIgnore));
elementsToIgnore.AddRange(elementsFromItemToIgnore);
var subElements = RecursiveGatherSubElementsInternal(item, properties);
result.MergeSubResult(subElements, hasJsonIgnore, isTypeRelatedToSharedObjects);
}
continue;
}
Expand All @@ -541,35 +602,32 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
{
foreach (var value in dict.Values)
{
elementsFromProperties.AddRange(RecursiveGatherSubElementsInternal(value, properties, out var elementsFromValueToIgnore));
elementsToIgnore.AddRange(elementsFromValueToIgnore);
var subElements = RecursiveGatherSubElementsInternal(value, properties);
result.MergeSubResult(subElements, hasJsonIgnore, isTypeRelatedToSharedObjects);
}
continue;
}

elementsFromProperties.AddRange(RecursiveGatherSubElementsInternal(pValue, properties, out var elementsFromPropertyToIgnore));
elementsToIgnore.AddRange(elementsFromPropertyToIgnore);
var gatheredSubElements = RecursiveGatherSubElementsInternal(pValue, properties);
result.MergeSubResult(gatheredSubElements, hasJsonIgnore, isTypeRelatedToSharedObjects);
}
catch (Exception ex)
{
throw new Exception($"The {p.Name} property or one of its children was not valid for introspection. Check the inner exception for details.", ex);
}
}
if (IsTypeRelatedToSharedObjects(t))
{
elementsToIgnore.AddRange(elementsFromProperties);
}
else

if (e != null)
{
elements.AddRange(elementsFromProperties);
result.Elements.Add(e);
}

if (e != null)
if (sharedObject != null)
{
elements.Add(e);
result.SharedObjects.Add(sharedObject);
}

return elements;
return result;
}

/// <summary>
Expand Down Expand Up @@ -624,7 +682,6 @@ internal static bool IsValidForRecursiveAddition(Type t)

private static bool IsTypeRelatedToSharedObjects(Type t)
{

return typeof(SharedObject).IsAssignableFrom(t)
|| typeof(RepresentationInstance).IsAssignableFrom(t);
}
Expand Down
Loading
Loading