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

Added IDisposable support for several classes #4939

Merged
merged 13 commits into from
Mar 24, 2020
25 changes: 24 additions & 1 deletion src/Microsoft.ML.Data/DataLoadSave/CompositeDataLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using Microsoft.ML;
using Microsoft.ML.Data;
using Microsoft.ML.Runtime;
Expand All @@ -15,7 +16,7 @@ namespace Microsoft.ML.Data
/// This class represents a data loader that applies a transformer chain after loading.
/// It also has methods to save itself to a repository.
/// </summary>
public sealed class CompositeDataLoader<TSource, TLastTransformer> : IDataLoader<TSource>
public sealed class CompositeDataLoader<TSource, TLastTransformer> : IDataLoader<TSource>, IDisposable
Copy link
Member

@sharwell sharwell Mar 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 By making the class disposable when IDataLoader<TSource> is not disposable, it will be easy for a consumer to fail to dispose of the object. #Resolved

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't change interfaces now that they are shipped. Right now, when customers discover they have a memory leak, we don't have a solution. With this method at least we can tell them to add a Dispose call.

Is there a better approach we can consider?


In reply to: 396132696 [](ancestors = 396132696)

Copy link
Member

@sharwell sharwell Mar 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every case that I reviewed locally required breaking API changes. Default interface methods might be an option for .NET Core targets, but those are completely unsupported on .NET Framework. #Resolved

Copy link
Contributor

@justinormont justinormont Mar 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the component GA? Or can we still make a breaking change? #Resolved

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All public interfaces are frozen. This would be one of those.


In reply to: 396753025 [](ancestors = 396753025)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All public interfaces are frozen. This would be one of those.

There may be a difference between interfaces released as GA and the preview releases. I'm not sure which one this falls under.

/cc @eerhardt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IDataLoader is a core interface. It is part of the GA set.


In reply to: 396760924 [](ancestors = 396760924)

where TLastTransformer : class, ITransformer
{
internal const string TransformerDirectory = TransformerChain.LoaderSignature;
Expand All @@ -38,13 +39,15 @@ public CompositeDataLoader(IDataLoader<TSource> loader, TransformerChain<TLastTr

Loader = loader;
Transformer = transformerChain ?? new TransformerChain<TLastTransformer>();
_disposed = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bools are initialized to false by the runtime. There is no need to re-initialize them to false here.

}

private CompositeDataLoader(IHost host, ModelLoadContext ctx)
{
if (!ctx.LoadModelOrNull<IDataLoader<TSource>, SignatureLoadModel>(host, out Loader, LegacyLoaderDirectory))
ctx.LoadModel<IDataLoader<TSource>, SignatureLoadModel>(host, out Loader, LoaderDirectory);
ctx.LoadModel<TransformerChain<TLastTransformer>, SignatureLoadModel>(host, out Transformer, TransformerDirectory);
_disposed = false;
}

private static CompositeDataLoader<TSource, TLastTransformer> Create(IHostEnvironment env, ModelLoadContext ctx)
Expand Down Expand Up @@ -110,5 +113,25 @@ private static VersionInfo GetVersionInfo()
loaderSignature: LoaderSignature,
loaderAssemblyName: typeof(CompositeDataLoader<,>).Assembly.FullName);
}

#region IDisposable Support
private bool _disposed;

private void Dispose(bool disposing)
Copy link
Member

@sharwell sharwell Mar 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 This is the legacy IDisposable pattern. The new way to implement this is moving this implementation code directly into IDisposable.Dispose(). #Resolved

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weren't there two cases to be handled before? The one that calls Dispose(true) from from IDisposable.Dispose() and the other that calls Dispose(false) from the finalizer? (Or someplace else...I am not quite sure). How would that be handled?


In reply to: 396132625 [](ancestors = 396132625)

Copy link
Member

@sharwell sharwell Mar 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the other that calls Dispose(false) from the finalizer

Finalizers are not allowed under the new dispose pattern. Since there are no finalizers, there are no calls to Dispose(false). Since there are no calls to dispose false, there is no need to add complexity by defining the method. #Resolved

{
if (_disposed)
return;

if (disposing)
Transformer.Dispose();

_disposed = true;
}

void IDisposable.Dispose()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you explicitly implementing IDisposable? Instead, you can just have a public void Dispose() method here.

The reason to not explicitly implement an interface is so users of the class can just call Dispose() as necessary. They don't need to cast to IDisposable to call it.

{
Dispose(true);
}
#endregion
}
}
29 changes: 28 additions & 1 deletion src/Microsoft.ML.Data/DataLoadSave/TransformerChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ internal interface ITransformerChainAccessor
/// A chain of transformers (possibly empty) that end with a <typeparamref name="TLastTransformer"/>.
/// For an empty chain, <typeparamref name="TLastTransformer"/> is always <see cref="ITransformer"/>.
/// </summary>
public sealed class TransformerChain<TLastTransformer> : ITransformer, IEnumerable<ITransformer>, ITransformerChainAccessor
public sealed class TransformerChain<TLastTransformer> : ITransformer, IEnumerable<ITransformer>, ITransformerChainAccessor, IDisposable
where TLastTransformer : class, ITransformer
{
private readonly ITransformer[] _transformers;
Expand Down Expand Up @@ -92,6 +92,7 @@ public TransformerChain(IEnumerable<ITransformer> transformers, IEnumerable<Tran

Contracts.Check((_transformers.Length > 0) == (LastTransformer != null));
Contracts.Check(_transformers.Length == _scopes.Length);
_disposed = false;
}

/// <summary>
Expand All @@ -116,6 +117,7 @@ public TransformerChain(params ITransformer[] transformers)
LastTransformer = transformers.Last() as TLastTransformer;
Contracts.Check(LastTransformer != null);
}
_disposed = false;
}

public DataViewSchema GetOutputSchema(DataViewSchema inputSchema)
Expand Down Expand Up @@ -232,6 +234,31 @@ IRowToRowMapper ITransformer.GetRowToRowMapper(DataViewSchema inputSchema)
}
return new CompositeRowToRowMapper(inputSchema, mappers);
}

#region IDisposable Support
private bool _disposed;

private void Dispose(bool disposing)
{
if (_disposed)
return;

if (disposing)
{
foreach (var transformer in _transformers)
(transformer as IDisposable)?.Dispose();

(LastTransformer as IDisposable)?.Dispose();
}

_disposed = true;
}

public void Dispose()
{
Dispose(true);
}
#endregion
}

/// <summary>
Expand Down
26 changes: 25 additions & 1 deletion src/Microsoft.ML.Data/DataView/CompositeRowToRowMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Microsoft.ML.Data
/// A row-to-row mapper that is the result of a chained application of multiple mappers.
/// </summary>
[BestFriend]
internal sealed class CompositeRowToRowMapper : IRowToRowMapper
internal sealed class CompositeRowToRowMapper : IRowToRowMapper, IDisposable
{
[BestFriend]
internal IRowToRowMapper[] InnerMappers { get; }
Expand All @@ -36,6 +36,7 @@ public CompositeRowToRowMapper(DataViewSchema inputSchema, IRowToRowMapper[] map
InnerMappers = Utils.Size(mappers) > 0 ? mappers : _empty;
InputSchema = inputSchema;
OutputSchema = Utils.Size(mappers) > 0 ? mappers[mappers.Length - 1].OutputSchema : inputSchema;
_disposed = false;
}

/// <summary>
Expand Down Expand Up @@ -118,5 +119,28 @@ public SubsetActive(DataViewRow row, Func<int, bool> pred)
/// </summary>
public override bool IsColumnActive(DataViewSchema.Column column) => _pred(column.Index);
}

#region IDisposable Support
private bool _disposed;

private void Dispose(bool disposing)
{
if (_disposed)
return;

if (disposing)
{
foreach (var mapper in InnerMappers)
(mapper as IDisposable)?.Dispose();
}

_disposed = true;
}

void IDisposable.Dispose()
{
Dispose(true);
}
#endregion
}
}
4 changes: 4 additions & 0 deletions src/Microsoft.ML.Data/Prediction/PredictionEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ private protected virtual void PredictionEngineCore(IHostEnvironment env, DataVi
var outputRowLocal = mapper.GetRow(inputRow, mapper.OutputSchema);
outputRow = cursorable.GetRow(outputRowLocal);
disposer = inputRow.Dispose;
//(mapper as IDisposable)?.Dispose();
}

private protected virtual Func<DataViewSchema, IRowToRowMapper> TransformerChecker(IExceptionContext ectx, ITransformer transformer)
Expand All @@ -168,7 +169,10 @@ private protected void Disposing(bool disposing)
if (_disposed)
return;
if (disposing)
{
_disposer?.Invoke();
(Transformer as IDisposable)?.Dispose();
}
_disposed = true;
}

Expand Down
26 changes: 25 additions & 1 deletion src/Microsoft.ML.Data/Scorers/MulticlassClassificationScorer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ private static VersionInfo GetVersionInfo()
/// </summary>
// REVIEW: It seems like the attachment of metadata should be solvable in a manner
// less ridiculously verbose than this.
public sealed class LabelNameBindableMapper : ISchemaBindableMapper, ICanSaveModel, IBindableCanSavePfa, IBindableCanSaveOnnx
public sealed class LabelNameBindableMapper : ISchemaBindableMapper, ICanSaveModel, IBindableCanSavePfa,
IBindableCanSaveOnnx, IDisposable
{
private static readonly FuncInstanceMethodInfo1<LabelNameBindableMapper, object, Delegate> _decodeInitMethodInfo
= FuncInstanceMethodInfo1<LabelNameBindableMapper, object, Delegate>.Create(target => target.DecodeInit<int>);
Expand Down Expand Up @@ -125,6 +126,7 @@ private LabelNameBindableMapper(IHostEnvironment env, ISchemaBindableMapper bind
_getter = getter;
_metadataKind = metadataKind;
_canWrap = canWrap;
_disposed = false;
}

private LabelNameBindableMapper(IHost host, ModelLoadContext ctx)
Expand All @@ -144,6 +146,7 @@ private LabelNameBindableMapper(IHost host, ModelLoadContext ctx)
_getter = Utils.MarshalInvoke(_decodeInitMethodInfo, this, _type.ItemType.RawType, value);
_metadataKind = ctx.Header.ModelVerReadable >= VersionAddedMetadataKind ?
ctx.LoadNonEmptyString() : AnnotationUtils.Kinds.SlotNames;
_disposed = false;
}

private Delegate DecodeInit<T>(object value)
Expand Down Expand Up @@ -379,6 +382,27 @@ public RowImpl(DataViewRow row, DataViewSchema schema)
public override ValueGetter<TValue> GetGetter<TValue>(DataViewSchema.Column column) => Input.GetGetter<TValue>(column);
}
}

#region IDisposable Support
private bool _disposed;

private void Dispose(bool disposing)
{
// TODO: Is it necessary to call the base class Dispose()?
if (_disposed)
return;

if (disposing)
(_bindable as IDisposable)?.Dispose();

_disposed = true;
}

void IDisposable.Dispose()
{
Dispose(true);
}
#endregion
}

/// <summary>
Expand Down
28 changes: 27 additions & 1 deletion src/Microsoft.ML.Data/Scorers/PredictedLabelScorerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace Microsoft.ML.Data
/// Class for scorers that compute on additional "PredictedLabel" column from the score column.
/// Currently, this scorer is used for binary classification, multi-class classification, and clustering.
/// </summary>
internal abstract class PredictedLabelScorerBase : RowToRowScorerBase, ITransformCanSavePfa, ITransformCanSaveOnnx
internal abstract class PredictedLabelScorerBase : RowToRowScorerBase, ITransformCanSavePfa, ITransformCanSaveOnnx, IDisposable
{
public abstract class ThresholdArgumentsBase : ScorerArgumentsBase
{
Expand Down Expand Up @@ -294,6 +294,7 @@ private protected PredictedLabelScorerBase(ScorerArgumentsBase args, IHostEnviro

Bindings = BindingsImpl.Create(data.Schema, rowMapper, args.Suffix, scoreColKind, scoreColIndex, predColType, predictedLabelColumnName);
OutputSchema = Bindings.AsSchema;
_disposed = false;
}

protected PredictedLabelScorerBase(IHostEnvironment env, PredictedLabelScorerBase transform,
Expand All @@ -302,6 +303,7 @@ protected PredictedLabelScorerBase(IHostEnvironment env, PredictedLabelScorerBas
{
Bindings = transform.Bindings.ApplyToSchema(newSource.Schema, Bindable, env);
OutputSchema = Bindings.AsSchema;
_disposed = false;
}

[BestFriend]
Expand All @@ -316,6 +318,7 @@ private protected PredictedLabelScorerBase(IHost host, ModelLoadContext ctx, IDa

Bindings = BindingsImpl.Create(ctx, input.Schema, host, Bindable, outputTypeMatches, getPredColType);
OutputSchema = Bindings.AsSchema;
_disposed = false;
}

private protected override void SaveCore(ModelSaveContext ctx)
Expand Down Expand Up @@ -435,5 +438,28 @@ protected void EnsureCachedPosition<TScore>(ref long cachedPosition, ref TScore
cachedPosition = boundRow.Position;
}
}

#region IDisposable Support
private bool _disposed;

protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;

if (disposing)
{
(Bindings.RowMapper as IDisposable)?.Dispose();
(Bindable as IDisposable)?.Dispose();
}

_disposed = true;
}

void IDisposable.Dispose()
{
Dispose(true);
}
#endregion
}
}
29 changes: 28 additions & 1 deletion src/Microsoft.ML.Data/Scorers/PredictionTransformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal static class PredictionTransformerBase
/// Base class for transformers with no feature column, or more than one feature columns.
/// </summary>
/// <typeparam name="TModel">The type of the model parameters used by this prediction transformer.</typeparam>
public abstract class PredictionTransformerBase<TModel> : IPredictionTransformer<TModel>
public abstract class PredictionTransformerBase<TModel> : IPredictionTransformer<TModel>, IDisposable
where TModel : class
{
/// <summary>
Expand Down Expand Up @@ -89,6 +89,7 @@ private protected PredictionTransformerBase(IHost host, TModel model, DataViewSc

Host.CheckValue(trainSchema, nameof(trainSchema));
TrainSchema = trainSchema;
_disposed = false;
}

[BestFriend]
Expand All @@ -103,6 +104,7 @@ private protected PredictionTransformerBase(IHost host, ModelLoadContext ctx)
Model = model;

InitializeLogic(host, ctx);
_disposed = false;
}

[BestFriend]
Expand All @@ -111,6 +113,7 @@ private protected PredictionTransformerBase(IHost host, ModelLoadContext ctx, TM
Host = host;
Model = model; // prediction model
InitializeLogic(host, ctx);
_disposed = false;
}

private void InitializeLogic(IHost host, ModelLoadContext ctx)
Expand Down Expand Up @@ -181,6 +184,30 @@ private protected void SaveModelCore(ModelSaveContext ctx)
}
});
}

#region IDisposable Support
private bool _disposed;

protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;

if (disposing)
{
(Model as IDisposable)?.Dispose();
(BindableMapper as IDisposable)?.Dispose();
(Scorer as IDisposable)?.Dispose();
}

_disposed = true;
}

void IDisposable.Dispose()
{
Dispose(true);
}
#endregion
}

/// <summary>
Expand Down
Loading