Intro

DeltaShell provides an implementation to run services remote (in another process / on another machine). The most common usage is to load the calculation kernel (typically fortran) of a model in a separate process to increase DeltaShell stability and prevent memory corruption issues when running a model several times. Another reason could be that you only have a 32bit version of a dll and want to run it from a 64bit process (Delta Shell).

The implementation is provided in a library which can be used stand alone (without Delta Shell) as well.

Typically when you are making a model (wrapper) in DeltaShell and you work with an external / native dll you should define a C# interface and a C# implementation of that interface (which in turn typically calls methods in a native dll), for example:

// model interface
public interface IMyModelApi
{
   void Initialize();
   void DoTimeStep();
   double GetValue(int location);
   void SetValue(int location, double value);
}

// implementation
public class MyModelApi : IMyModelApi
{
   public void Initialize() {/*implementation*/}
   public void DoTimeStep() {/*implementation*/}
   public double GetValue(int location)
   {
      // typically interaction with native dll
   }

   void SetValue(int location, double value)
   {
      // typically interaction with native dll
   }
}

You would normally create an instance of the implementation like this to use in your model wrapper:

IMyModelApi api = new MyModelApi();

// usage:
public void Initialize
{
  api.SetValue(pumpId, 5.0); //set initial value
  api.Initialize();
}

One of the disadvantages of this approach is that if a native dll is loaded using DllImport, it will never be unloaded until the process (DeltaShell) exits. As a consequence, any memory leaks in the native dll will build up in the parent process, omissions in re-initialization may cause model corruption, or in general: previous runs may affect the current run. Most notably, when the dll aborts / causes an access violation, it will instantly crash the entire parent process (eg DeltaShell) without chance of recovery.

What is typically done to work around these issues, is that for each model-run a new 'worker' process is spawned. That worker process then loads the native dll, does the model calculations and then exits. For subsequent runs a new process is launched, thus making sure each run is 'clean'. To allow for interaction during the run, the DeltaShell process and the worker process need to communicate, which is called 'remoting'. The worker process may run on the local machine, but could also run on another machine.

Solution

To hide all the complexities of remoting, Delta Shell provides a few simple methods to work with a 'remote' version of your api. Typically you will have to change very little in your model code if you already have a well defined interface for your api.

So if you previously did this:

IMyModelApi api = new MyModelApi();
api.Initialize();

You can now do this:

IMyModelApi api = RemoteInstanceContainer.CreateInstance<IMyModelApi, MyModelApi>();
api.Initialize();

And you can now use the 'api' variable just like before, but behind the scenes DeltaShell has started a new process and each call you make to the api is in fact intercepted and delegated to the worker process.

There is one extra step you have to take to clean things up when you're done with the run:

RemoteInstanceContainer.RemoveInstance(api);

This will tell DeltaShell to end the worker process and cleanup any resources. After this the api variable is no longer usable. Next run you should call CreateInstance again.

For examples see the following usages in DeltaShell: IModelApi, IRRModelApi, IXBeachApi, PcRasterModel or RemoteInstanceContainerTest

Requirements on the interface

The interface you use to communicate (IMyModelApi in the example above) should follow a few basic rules to work smoothly with the remoting implementation. Any interface will do: no special attributes are required, but you should try to use mostly simple (value) types (as is common when defining an interface of a native dlls as well).

Default supported types:

  • all primitive types (int, double, string, etc)
  • enums
  • NOT decimal
  • double[], bool[], int[], short[], float[], byte[], string[]
  • 'Array' if of above type at runtime!
  • ref/out keywords on above types. Only ref on the array types.
  • Type, DateTime, TimeSpan, DateTime[] (through type converter)
  • ..more through type converters
  • ..more support can be added in the code, ask!

To support custom classes a little bit of extra work is required. You can either annotate the class with the required attributes for serialization (see below), or you can introduce a type converter which does custom serialization (see DateTimeToProtoConverter.cs). The latter is useful if you do not have access to the source code (eg, a .NET object) or do not wish to mix remoting and domain.

To work without a type converter, you can annotate a class as follows:

    [ProtoContract]
    public class CurvilinearGrid
    {
        [ProtoMember(1)] public int SizeN;
        [ProtoMember(2)] public int SizeM;

        [ProtoMember(3)] public double[] X;
        [ProtoMember(4)] public double[] Y;
        // additional code
    }

The attributes come from the protobuf .net library and the annotation should be done according to their specification. In short; the class should have the ProtoContract attribute and each member you want to serialize to the other process should have the ProtoMember attribute with a unique number. Only non primitive or proto-annotated classes can be members. Also the class should have a default constructor.

Performance considerations

Running code in another process obviously has some performance overhead when compared to running it in the same process. The overhead comes from starting the other process, intercepting the calls, serializing the call & data into bytes, sending the information to the other process, decoding the information on the other end, doing the work, serializing the resulting data into bytes, sending the data back, and then decoding the result value(s). Although the absolute time to do all this work is actually quite small (say, 1 millisecond per call), it may significantly impact your performance depending on how many calls you are making versus the amount of work being done per call. Also if your parameters or result values are large (eg, arrays of data), the time it takes to serialize and deserialize the data increases and/or could lead to increased memory overhead. When running the remote instance on the same machine, DeltaShell counters this problem by using a technique called 'Shared Memory' to transfer large arrays more efficiently. In short; it doesn't serialize the array, instead it just memcpy's it between processes. This happens automatically when the array size exceeds some threshold and requires no additional configuration. It only works for one dimensional arrays however and also not for arrays defined inside custom types.

So, to summarize, take this into consideration:

  • More work / less calls is better (see for example IRemoteRtcToolsDllWrapper on how multiple calls can be combined into a single call)
  • Send large multi dimensional arrays as one dimensional
  • Avoid large arrays in custom types as they aren't optimized
  • If your dll writes to the console alot (eg write(,)), this can have significant impact on the performance, even if the console is not visible. Try again with debug messages off!
  • If in doubt, ask! The code can be changed / extended to support more options!

Additionally for your purpose it might be beneficial to re-use remote instances, start them in advance (warmup), or run multiple concurrent!

  • No labels