From 0980982095e5fc99a20b627c188d52a8b63248c7 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Thu, 14 Aug 2008 20:38:07 +0100 Subject: Evil reflection optimisation. --- csharp/ProtocolBuffers/FieldAccess/Delegates.cs | 23 +++- .../ProtocolBuffers/FieldAccess/IFieldAccessor.cs | 8 +- .../ProtocolBuffers/FieldAccess/ReflectionUtil.cs | 118 +++++++++++++++++++++ .../FieldAccess/RepeatedEnumAccessor.cs | 6 +- .../FieldAccess/RepeatedMessageAccessor.cs | 11 +- .../FieldAccess/RepeatedPrimitiveAccessor.cs | 36 ++++--- .../FieldAccess/SingleMessageAccessor.cs | 11 +- .../FieldAccess/SinglePrimitiveAccessor.cs | 26 +++-- csharp/ProtocolBuffers/GeneratedBuilder.cs | 6 +- csharp/ProtocolBuffers/GeneratedMessage.cs | 2 +- csharp/ProtocolBuffers/ProtocolBuffers.csproj | 1 + 11 files changed, 197 insertions(+), 51 deletions(-) create mode 100644 csharp/ProtocolBuffers/FieldAccess/ReflectionUtil.cs (limited to 'csharp') diff --git a/csharp/ProtocolBuffers/FieldAccess/Delegates.cs b/csharp/ProtocolBuffers/FieldAccess/Delegates.cs index 04b1d94c..e6fb4fe8 100644 --- a/csharp/ProtocolBuffers/FieldAccess/Delegates.cs +++ b/csharp/ProtocolBuffers/FieldAccess/Delegates.cs @@ -1,11 +1,28 @@ -using System; -using System.Collections.Generic; -using System.Text; +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. +// http://code.google.com/p/protobuf/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. namespace Google.ProtocolBuffers.FieldAccess { + // TODO(jonskeet): Convert these to Func/Action family delegate bool HasDelegate(T message); delegate T ClearDelegate(T builder); delegate int RepeatedCountDelegate(T message); delegate object GetValueDelegate(T message); + delegate void SingleValueDelegate(TSource source, object value); + delegate IBuilder CreateBuilderDelegate(); + delegate object GetIndexedValueDelegate(T message, int index); + delegate object SetIndexedValueDelegate(T message, int index, object value); } diff --git a/csharp/ProtocolBuffers/FieldAccess/IFieldAccessor.cs b/csharp/ProtocolBuffers/FieldAccess/IFieldAccessor.cs index eb57c8c9..ec079f92 100644 --- a/csharp/ProtocolBuffers/FieldAccess/IFieldAccessor.cs +++ b/csharp/ProtocolBuffers/FieldAccess/IFieldAccessor.cs @@ -58,18 +58,18 @@ namespace Google.ProtocolBuffers.FieldAccess { /// /// Accessor for repeated fields /// - object GetRepeatedValue(IMessage message, int index); + object GetRepeatedValue(TMessage message, int index); /// /// Mutator for repeated fields /// - void SetRepeated(IBuilder builder, int index, object value); + void SetRepeated(TBuilder builder, int index, object value); /// /// Adds the specified value to the field in the given builder. /// - void AddRepeated(IBuilder builder, object value); + void AddRepeated(TBuilder builder, object value); /// /// Returns a read-only wrapper around the value of a repeated field. /// - object GetRepeatedWrapper(IBuilder builder); + object GetRepeatedWrapper(TBuilder builder); } } diff --git a/csharp/ProtocolBuffers/FieldAccess/ReflectionUtil.cs b/csharp/ProtocolBuffers/FieldAccess/ReflectionUtil.cs new file mode 100644 index 00000000..ca768a76 --- /dev/null +++ b/csharp/ProtocolBuffers/FieldAccess/ReflectionUtil.cs @@ -0,0 +1,118 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. +// http://code.google.com/p/protobuf/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +using System; +using System.Reflection; + +namespace Google.ProtocolBuffers.FieldAccess { + + /// + /// The methods in this class are somewhat evil, and should not be tampered with lightly. + /// Basically they allow the creation of relatively weakly typed delegates from MethodInfos + /// which are more strongly typed. They do this by creating an appropriate strongly typed + /// delegate from the MethodInfo, and then calling that within an anonymous method. + /// Mind-bending stuff (at least to your humble narrator) but the resulting delegates are + /// very fast compared with calling Invoke later on. + /// + internal static class ReflectionUtil { + + /// + /// Creates a delegate which will execute the given method and then return + /// the result as an object. + /// + public static GetValueDelegate CreateUpcastDelegate(MethodInfo method) { + + // The tricky bit is invoking CreateCreateUpcastDelegateImpl with the right type parameters + MethodInfo openImpl = typeof(ReflectionUtil).GetMethod("CreateUpcastDelegateImpl"); + MethodInfo closedImpl = openImpl.MakeGenericMethod(typeof(T), method.ReturnType); + return (GetValueDelegate) closedImpl.Invoke(null, new object[] { method }); + } + + delegate TResult Getter(TSource source); + + /// + /// Method used solely for implementing CreateUpcastDelegate. Public to avoid trust issues + /// in low-trust scenarios, e.g. Silverlight. + /// TODO(jonskeet): Check any of this actually works in Silverlight... + /// + public static GetValueDelegate CreateUpcastDelegateImpl(MethodInfo method) { + // Convert the reflection call into an open delegate, i.e. instead of calling x.Method() + // we'll call getter(x). + Getter getter = (Getter)Delegate.CreateDelegate(typeof(Getter), method); + + // Implicit upcast to object (within the delegate) + return delegate(TSource source) { return getter(source); }; + } + + + /// + /// Creates a delegate which will execute the given method after casting the parameter + /// down from object to the required parameter type. + /// + public static SingleValueDelegate CreateDowncastDelegate(MethodInfo method) { + MethodInfo openImpl = typeof(ReflectionUtil).GetMethod("CreateDowncastDelegateImpl"); + MethodInfo closedImpl = openImpl.MakeGenericMethod(typeof(T), method.GetParameters()[0].ParameterType); + return (SingleValueDelegate)closedImpl.Invoke(null, new object[] { method }); + } + + delegate void OpenSingleValueDelegate(TSource source, TParam parameter); + + public static SingleValueDelegate CreateDowncastDelegateImpl(MethodInfo method) { + // Convert the reflection call into an open delegate, i.e. instead of calling x.Method(y) we'll + // call Method(x, y) + OpenSingleValueDelegate call = (OpenSingleValueDelegate) + Delegate.CreateDelegate(typeof(OpenSingleValueDelegate), method); + + return delegate(TSource source, object parameter) { call(source, (TParam)parameter); }; + } + + /// + /// Creates a delegate which will execute the given method after casting the parameter + /// down from object to the required parameter type. + /// + public static SingleValueDelegate CreateDowncastDelegateIgnoringReturn(MethodInfo method) { + MethodInfo openImpl = typeof(ReflectionUtil).GetMethod("CreateDowncastDelegateIgnoringReturnImpl"); + MethodInfo closedImpl = openImpl.MakeGenericMethod(typeof(T), method.GetParameters()[0].ParameterType, method.ReturnType); + return (SingleValueDelegate)closedImpl.Invoke(null, new object[] { method }); + } + + delegate TReturn OpenSingleValueDelegate(TSource source, TParam parameter); + + public static SingleValueDelegate CreateDowncastDelegateIgnoringReturnImpl(MethodInfo method) { + // Convert the reflection call into an open delegate, i.e. instead of calling x.Method(y) we'll + // call Method(x, y) + OpenSingleValueDelegate call = (OpenSingleValueDelegate) + Delegate.CreateDelegate(typeof(OpenSingleValueDelegate), method); + + return delegate(TSource source, object parameter) { call(source, (TParam)parameter); }; + } + + delegate T OpenCreateBuilderDelegate(); + + /// + /// Creates a delegate which will execute the given static method and cast the result up to IBuilder. + /// + public static CreateBuilderDelegate CreateStaticUpcastDelegate(MethodInfo method) { + MethodInfo openImpl = typeof(ReflectionUtil).GetMethod("CreateStaticUpcastDelegateImpl"); + MethodInfo closedImpl = openImpl.MakeGenericMethod(method.ReturnType); + return (CreateBuilderDelegate) closedImpl.Invoke(null, new object[] { method }); + } + + public static CreateBuilderDelegate CreateStaticUpcastDelegateImpl(MethodInfo method) { + OpenCreateBuilderDelegate call = (OpenCreateBuilderDelegate)Delegate.CreateDelegate(typeof(OpenCreateBuilderDelegate), method); + return delegate { return (IBuilder)call(); }; + } + } +} diff --git a/csharp/ProtocolBuffers/FieldAccess/RepeatedEnumAccessor.cs b/csharp/ProtocolBuffers/FieldAccess/RepeatedEnumAccessor.cs index 3082071b..f00f7ee2 100644 --- a/csharp/ProtocolBuffers/FieldAccess/RepeatedEnumAccessor.cs +++ b/csharp/ProtocolBuffers/FieldAccess/RepeatedEnumAccessor.cs @@ -42,18 +42,18 @@ namespace Google.ProtocolBuffers.FieldAccess { return Lists.AsReadOnly(ret); } - public override object GetRepeatedValue(IMessage message, int index) { + public override object GetRepeatedValue(TMessage message, int index) { // Note: This relies on the fact that the CLR allows unboxing from an enum to // its underlying value int rawValue = (int) base.GetRepeatedValue(message, index); return enumDescriptor.FindValueByNumber(rawValue); } - public override void AddRepeated(IBuilder builder, object value) { + public override void AddRepeated(TBuilder builder, object value) { base.AddRepeated(builder, ((EnumValueDescriptor) value).Number); } - public override void SetRepeated(IBuilder builder, int index, object value) { + public override void SetRepeated(TBuilder builder, int index, object value) { base.SetRepeated(builder, index, ((EnumValueDescriptor) value).Number); } } diff --git a/csharp/ProtocolBuffers/FieldAccess/RepeatedMessageAccessor.cs b/csharp/ProtocolBuffers/FieldAccess/RepeatedMessageAccessor.cs index f350752d..5c62782b 100644 --- a/csharp/ProtocolBuffers/FieldAccess/RepeatedMessageAccessor.cs +++ b/csharp/ProtocolBuffers/FieldAccess/RepeatedMessageAccessor.cs @@ -33,13 +33,14 @@ namespace Google.ProtocolBuffers.FieldAccess { /// in a message type "Foo", a field called "bar" might be of type "Baz". This /// method is Baz.CreateBuilder. /// - private readonly MethodInfo createBuilderMethod; + private readonly CreateBuilderDelegate createBuilderDelegate; internal RepeatedMessageAccessor(string name) : base(name) { - createBuilderMethod = ClrType.GetMethod("CreateBuilder", new Type[0]); + MethodInfo createBuilderMethod = ClrType.GetMethod("CreateBuilder", new Type[0]); if (createBuilderMethod == null) { throw new ArgumentException("No public static CreateBuilder method declared in " + ClrType.Name); } + createBuilderDelegate = ReflectionUtil.CreateStaticUpcastDelegate(createBuilderMethod); } /// @@ -58,15 +59,15 @@ namespace Google.ProtocolBuffers.FieldAccess { return CreateBuilder().WeakMergeFrom(message).WeakBuild(); } - public override void SetRepeated(IBuilder builder, int index, object value) { + public override void SetRepeated(TBuilder builder, int index, object value) { base.SetRepeated(builder, index, CoerceType(value)); } public override IBuilder CreateBuilder() { - return (IBuilder) createBuilderMethod.Invoke(null, null); + return createBuilderDelegate(); } - public override void AddRepeated(IBuilder builder, object value) { + public override void AddRepeated(TBuilder builder, object value) { base.AddRepeated(builder, CoerceType(value)); } } diff --git a/csharp/ProtocolBuffers/FieldAccess/RepeatedPrimitiveAccessor.cs b/csharp/ProtocolBuffers/FieldAccess/RepeatedPrimitiveAccessor.cs index 39184b1f..0761635a 100644 --- a/csharp/ProtocolBuffers/FieldAccess/RepeatedPrimitiveAccessor.cs +++ b/csharp/ProtocolBuffers/FieldAccess/RepeatedPrimitiveAccessor.cs @@ -25,13 +25,15 @@ namespace Google.ProtocolBuffers.FieldAccess { where TMessage : IMessage where TBuilder : IBuilder { - private readonly PropertyInfo messageProperty; - private readonly PropertyInfo builderProperty; - private readonly RepeatedCountDelegate countDelegate; + private readonly Type clrType; + private readonly GetValueDelegate getValueDelegate; private readonly ClearDelegate clearDelegate; - private readonly MethodInfo addMethod; + private readonly SingleValueDelegate addValueDelegate; + private readonly GetValueDelegate getRepeatedWrapperDelegate; + private readonly RepeatedCountDelegate countDelegate; private readonly MethodInfo getElementMethod; private readonly MethodInfo setElementMethod; + /// /// The CLR type of the field (int, the enum type, ByteString, the message etc). @@ -39,16 +41,17 @@ namespace Google.ProtocolBuffers.FieldAccess { /// value. /// protected Type ClrType { - get { return getElementMethod.ReturnType; } + get { return clrType; } } internal RepeatedPrimitiveAccessor(string name) { - messageProperty = typeof(TMessage).GetProperty(name + "List"); - builderProperty = typeof(TBuilder).GetProperty(name + "List"); + PropertyInfo messageProperty = typeof(TMessage).GetProperty(name + "List"); + PropertyInfo builderProperty = typeof(TBuilder).GetProperty(name + "List"); PropertyInfo countProperty = typeof(TMessage).GetProperty(name + "Count"); MethodInfo clearMethod = typeof(TBuilder).GetMethod("Clear" + name); getElementMethod = typeof(TMessage).GetMethod("Get" + name, new Type[] { typeof(int) }); - addMethod = typeof(TBuilder).GetMethod("Add" + name, new Type[] { ClrType }); + clrType = getElementMethod.ReturnType; + MethodInfo addMethod = typeof(TBuilder).GetMethod("Add" + name, new Type[] { ClrType }); setElementMethod = typeof(TBuilder).GetMethod("Set" + name, new Type[] { typeof(int), ClrType }); if (messageProperty == null || builderProperty == null @@ -62,6 +65,9 @@ namespace Google.ProtocolBuffers.FieldAccess { clearDelegate = (ClearDelegate)Delegate.CreateDelegate(typeof(ClearDelegate), clearMethod); countDelegate = (RepeatedCountDelegate)Delegate.CreateDelegate (typeof(RepeatedCountDelegate), countProperty.GetGetMethod()); + getValueDelegate = ReflectionUtil.CreateUpcastDelegate(messageProperty.GetGetMethod()); + addValueDelegate = ReflectionUtil.CreateDowncastDelegateIgnoringReturn(addMethod); + getRepeatedWrapperDelegate = ReflectionUtil.CreateUpcastDelegate(builderProperty.GetGetMethod()); } public bool Has(TMessage message) { @@ -73,7 +79,7 @@ namespace Google.ProtocolBuffers.FieldAccess { } public virtual object GetValue(TMessage message) { - return messageProperty.GetValue(message, null); + return getValueDelegate(message); } public void SetValue(TBuilder builder, object value) { @@ -95,24 +101,24 @@ namespace Google.ProtocolBuffers.FieldAccess { return countDelegate(message); } - public virtual object GetRepeatedValue(IMessage message, int index) { + public virtual object GetRepeatedValue(TMessage message, int index) { return getElementMethod.Invoke(message, new object[] {index } ); } - public virtual void SetRepeated(IBuilder builder, int index, object value) { + public virtual void SetRepeated(TBuilder builder, int index, object value) { setElementMethod.Invoke(builder, new object[] {index, value} ); } - public virtual void AddRepeated(IBuilder builder, object value) { - addMethod.Invoke(builder, new object[] { value }); + public virtual void AddRepeated(TBuilder builder, object value) { + addValueDelegate(builder, value); } /// /// The builder class's accessor already builds a read-only wrapper for /// us, which is exactly what we want. /// - public object GetRepeatedWrapper(IBuilder builder) { - return builderProperty.GetValue(builder, null); + public object GetRepeatedWrapper(TBuilder builder) { + return getRepeatedWrapperDelegate(builder); } } } \ No newline at end of file diff --git a/csharp/ProtocolBuffers/FieldAccess/SingleMessageAccessor.cs b/csharp/ProtocolBuffers/FieldAccess/SingleMessageAccessor.cs index 460d450d..fd6b5c9b 100644 --- a/csharp/ProtocolBuffers/FieldAccess/SingleMessageAccessor.cs +++ b/csharp/ProtocolBuffers/FieldAccess/SingleMessageAccessor.cs @@ -29,15 +29,14 @@ namespace Google.ProtocolBuffers.FieldAccess { /// in a message type "Foo", a field called "bar" might be of type "Baz". This /// method is Baz.CreateBuilder. /// - private readonly MethodInfo createBuilderMethod; + private readonly CreateBuilderDelegate createBuilderDelegate; - - internal SingleMessageAccessor(string name) : base(name) { - - createBuilderMethod = ClrType.GetMethod("CreateBuilder", new Type[0]);//BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); + internal SingleMessageAccessor(string name) : base(name) { + MethodInfo createBuilderMethod = ClrType.GetMethod("CreateBuilder", new Type[0]); if (createBuilderMethod == null) { throw new ArgumentException("No public static CreateBuilder method declared in " + ClrType.Name); } + createBuilderDelegate = ReflectionUtil.CreateStaticUpcastDelegate(createBuilderMethod); } /// @@ -61,7 +60,7 @@ namespace Google.ProtocolBuffers.FieldAccess { } public override IBuilder CreateBuilder() { - return (IBuilder) createBuilderMethod.Invoke(null, null); + return createBuilderDelegate(); } } } \ No newline at end of file diff --git a/csharp/ProtocolBuffers/FieldAccess/SinglePrimitiveAccessor.cs b/csharp/ProtocolBuffers/FieldAccess/SinglePrimitiveAccessor.cs index 8d12b45c..6c797370 100644 --- a/csharp/ProtocolBuffers/FieldAccess/SinglePrimitiveAccessor.cs +++ b/csharp/ProtocolBuffers/FieldAccess/SinglePrimitiveAccessor.cs @@ -24,8 +24,9 @@ namespace Google.ProtocolBuffers.FieldAccess { where TMessage : IMessage where TBuilder : IBuilder { - private readonly PropertyInfo messageProperty; - private readonly PropertyInfo builderProperty; + private readonly Type clrType; + private readonly GetValueDelegate getValueDelegate; + private readonly SingleValueDelegate setValueDelegate; private readonly HasDelegate hasDelegate; private readonly ClearDelegate clearDelegate; @@ -34,19 +35,22 @@ namespace Google.ProtocolBuffers.FieldAccess { /// As declared by the property. /// protected Type ClrType { - get { return messageProperty.PropertyType; } + get { return clrType; } } internal SinglePrimitiveAccessor(string name) { - messageProperty = typeof(TMessage).GetProperty(name); - builderProperty = typeof(TBuilder).GetProperty(name); + PropertyInfo messageProperty = typeof(TMessage).GetProperty(name); + PropertyInfo builderProperty = typeof(TBuilder).GetProperty(name); PropertyInfo hasProperty = typeof(TMessage).GetProperty("Has" + name); MethodInfo clearMethod = typeof(TBuilder).GetMethod("Clear" + name); if (messageProperty == null || builderProperty == null || hasProperty == null || clearMethod == null) { throw new ArgumentException("Not all required properties/methods available"); } + clrType = messageProperty.PropertyType; hasDelegate = (HasDelegate)Delegate.CreateDelegate(typeof(HasDelegate), hasProperty.GetGetMethod()); clearDelegate = (ClearDelegate)Delegate.CreateDelegate(typeof(ClearDelegate), clearMethod); + getValueDelegate = ReflectionUtil.CreateUpcastDelegate(messageProperty.GetGetMethod()); + setValueDelegate = ReflectionUtil.CreateDowncastDelegate(builderProperty.GetSetMethod()); } public bool Has(TMessage message) { @@ -65,11 +69,11 @@ namespace Google.ProtocolBuffers.FieldAccess { } public virtual object GetValue(TMessage message) { - return messageProperty.GetValue(message, null); + return getValueDelegate(message); } public virtual void SetValue(TBuilder builder, object value) { - builderProperty.SetValue(builder, value, null); + setValueDelegate(builder, value); } #region Methods only related to repeated values @@ -77,19 +81,19 @@ namespace Google.ProtocolBuffers.FieldAccess { throw new InvalidOperationException(); } - public object GetRepeatedValue(IMessage message, int index) { + public object GetRepeatedValue(TMessage message, int index) { throw new InvalidOperationException(); } - public void SetRepeated(IBuilder builder, int index, object value) { + public void SetRepeated(TBuilder builder, int index, object value) { throw new InvalidOperationException(); } - public void AddRepeated(IBuilder builder, object value) { + public void AddRepeated(TBuilder builder, object value) { throw new InvalidOperationException(); } - public object GetRepeatedWrapper(IBuilder builder) { + public object GetRepeatedWrapper(TBuilder builder) { throw new InvalidOperationException(); } #endregion diff --git a/csharp/ProtocolBuffers/GeneratedBuilder.cs b/csharp/ProtocolBuffers/GeneratedBuilder.cs index 54d37392..a1c59e89 100644 --- a/csharp/ProtocolBuffers/GeneratedBuilder.cs +++ b/csharp/ProtocolBuffers/GeneratedBuilder.cs @@ -51,7 +51,7 @@ namespace Google.ProtocolBuffers { // For repeated fields, the underlying list object is still modifiable at this point. // Make sure not to expose the modifiable list to the caller. return field.IsRepeated - ? InternalFieldAccessors[field].GetRepeatedWrapper(this) + ? InternalFieldAccessors[field].GetRepeatedWrapper(ThisBuilder) : MessageBeingBuilt[field]; } set { @@ -92,7 +92,7 @@ namespace Google.ProtocolBuffers { public override object this[FieldDescriptor field, int index] { get { return MessageBeingBuilt[field, index]; } - set { InternalFieldAccessors[field].SetRepeated(this, index, value); } + set { InternalFieldAccessors[field].SetRepeated(ThisBuilder, index, value); } } public override bool HasField(FieldDescriptor field) { @@ -144,7 +144,7 @@ namespace Google.ProtocolBuffers { } public override TBuilder AddRepeatedField(FieldDescriptor field, object value) { - InternalFieldAccessors[field].AddRepeated(this, value); + InternalFieldAccessors[field].AddRepeated(ThisBuilder, value); return ThisBuilder; } diff --git a/csharp/ProtocolBuffers/GeneratedMessage.cs b/csharp/ProtocolBuffers/GeneratedMessage.cs index 0fd54f6c..6198afbf 100644 --- a/csharp/ProtocolBuffers/GeneratedMessage.cs +++ b/csharp/ProtocolBuffers/GeneratedMessage.cs @@ -112,7 +112,7 @@ namespace Google.ProtocolBuffers { } public override object this[FieldDescriptor field, int index] { - get { return InternalFieldAccessors[field].GetRepeatedValue(this, index); } + get { return InternalFieldAccessors[field].GetRepeatedValue(ThisMessage, index); } } public override object this[FieldDescriptor field] { diff --git a/csharp/ProtocolBuffers/ProtocolBuffers.csproj b/csharp/ProtocolBuffers/ProtocolBuffers.csproj index c057b414..88a21226 100644 --- a/csharp/ProtocolBuffers/ProtocolBuffers.csproj +++ b/csharp/ProtocolBuffers/ProtocolBuffers.csproj @@ -72,6 +72,7 @@ + -- cgit v1.2.3