AU 2010 Handout: Integrate F# into Your C# or VB.NET Application for an 8x Performance Boost

This handout is for the companion class to the one whose handout formed my last post. While that class was user-focused, this one, "CP322-2 - Integrate F# into Your C# or VB.NET Application for an 8x Performance Boost", is more developer-focused and takes the hood off the implementation of the BrowsePhotosynth application. The code for this special version of the application – which imports synchronously via C# and synchronously/asynchronously via F# – is available here for download.

Introduction

This class takes a look at the implementation of BrowsePhotosynth for AutoCAD, the ADN Plugin of the Month from October 2010 and the application showcased in the companion, user-oriented class, "AC427-4 - Point Clouds on a Shoestring". We'll look at some of the design decisions behind the application, particularly with respect to the use of F# to help download and process the various files making up a Photosynth's point cloud.

To understand more about the purpose of this application, it's first worth taking a look at the handout to the companion class. In a nutshell, the BrowsePhotosynth application allows users of AutoCAD 2011 (and, in due course, above) to browse the contents of the Photosynth web-service and easily bring down point clouds corresponding to the "synths" hosted on the site.

The System Architecture

Let's take a look at the overall architecture of the system before diving into the individual components.

Component architectureHere we can see a few DLLs are hosted by AutoCAD: the primary one is called ADNPlugin-BrowsePhotosynth.dll and implements a number of commands, the most important of which are BROWSEPS and IMPORTPS. It is this DLL that needs to be NETLOADed or  demand-loaded into AutoCAD for the BrowsePhotosynth application to work properly.

Users will call BROWSEPS, which causes the UI component - ADNPlugin-PhotosynthBrowser.exe – to be launched to present a dialog user to the user. This dialog will – in turn – call the IMPORTPS back in AutoCAD to download, process and import the point cloud data. It's during this processing phase that the main Importer may choose to use a separate component, the Processor DLL (ADNPlugin-PhotosynthProcessor.dll). This functionality has been packaged as a separate component as it was written in F#.

The User Interface

As mentioned above, the main entry-point into the application is the BROWSEPS command, which launches a WPF dialog implemented in the "Browser" project of the main solution.

The BrowsePhotosynth dialog in AutoCAD 2011 This project actually builds an EXE (ADNPlugin-PhotosynthBrowser.exe), rather than a DLL, which is interesting for a few reasons:

  • Isolation from AutoCAD
    • This 32-bit EXE can be executed via WoW64 on 64-bit systems (more later on why this is important)
    • The memory footprint is managed separately – it doesn't contribute to the size of AutoCAD's process space
  • Portability to other products
    • As we support point clouds across more of our products, having an independent GUI component will simplify migration to support them
  • Can even by used standalone
    • As the browser does not depend on AutoCAD, it can also be executed separately, allowing the user to build up the lost of point clouds to import before even launching AutoCAD

The overriding reason for this design was the first: the ability to run as 32-bit even on 64-bit OSs was important as we make use of a 3rd party component, csExWb2, which is currently only available as a 32-bit version. This component also causes an error when the hosting dialog closes (which you will see if you launch the EXE separately, as described in the third point above). It's also for this reason – to isolate the user from this error, which cause AutoCAD stability problems when it happened in an in-process component – that the dialog is hosted in a separate application. The main plugin launches and maintains an instance of the dialog's process: when the dialog is closed it is effectively hidden and the process gets terminated at an appropriate point later on, thus avoiding the error. If AutoCAD gets terminated unexpectedly (such as via the debugger), there may be a stray process which is detected and terminated (should the user request it) when the application is next used.

So why used a 3rd party component, if it causes all this problems? The component implements a web-browser control that reports the HTTP traffic generated by its contents to the application. This allows us to detect when the user has visited a page containing a synth, as the embedded Silverlight control requests a file named "points_0_0.bin", the file containing the point cloud's first 5,000 points. There may be other browser controls available that implement this capability, but I unfortunately haven't found any.

As the application detects point clouds being accessed, they get added to the list on the right of the dialog using an image that is also pulled down from the Photosynth server.

The original UI was implemented using WinForms, but the benefits of using WPF quickly became apparent as additional UI features – such as the ability to resize the items using a slider – became desirable. It's worth taking a look at some of the techniques used in the WPF application – here are some of them:

  • Our list of point clouds is bound to an ObservableCollection<> of objects containing the information we care about
    • We had to implement INotifyPropertyChanged for this to work properly
    • We add items to this list when our HTTP event is fired, but as we're not executing on the UI thread, at that point, we need to request the list to be updated via Dispatcher.Invoke()
  • We use a WindowInteropHelper to make AutoCAD's main window the parent of the EXE
    • This gives a modal – rather than modeless – feel to the UI
  • The display of our listbox has been customized significantly
    • It selects on hover
    • It has a custom gradient fill to better fit the look & feel of the Photosynth site

For additional information regarding the development of the WPF UI for this application, see this blog post.

Now on to how the UI communicates with AutoCAD. To avoid any dependency on AutoCAD from the WPF application, it simply uses Windows messages to launch the IMPORTPS command. This decoupling is healthy for portability reasons but also to make sure the command gets launched cleanly: it is considered best practice to launch commands in AutoCAD from a modeless UI, whether using AutoCAD's SendStringToExecute() API or using Win32's SendMessage().

The Import Process

The IMPORTPS command – which is actually the real heart of the application – has the following process:

BrowsePhotosynth flow chart

It's worth noting that the application doesn't actually make much use of AutoCAD's APIs: it uses SendStringToExecute() to fire off standard commands to index and attach the point cloud, but aside from that the plugin's code is also fairly standalone in nature.

We're most interested with the left-hand part of this process, where we bring the .bin files down from the Photosynth server and process them into a single text file. Let's start by understanding why there are all these files to download, process and combine.

Photosynth's web service stores point clouds in chunks of 5,000 points. So if we have a point cloud comprising 26,000 points it will be stored in 6 files named points_0_0.bin, points_0_1.bin, points_0_2.bin, points_0_3.bin, points_0_4.bin and points_0_5.bin, each containing 5,000 points, apart from the last which will only contain 1,000. This has most probably been done to enable the Silverlight control to stream down sections of the point cloud selectively/progressively. There may be additional point clouds (contained in files such as points_2_0.bin, etc.), but as these do not share a coordinate system with the primary point cloud, including them only proves confusing. These could easily be downloaded and combined into separate PCG files inside AutoCAD, but this has been left as an exercise for the user, as the merit of doing so seems somewhat dubious.

Now onto the main purpose of this class. 🙂

Given the unordered nature of point clouds – at least from AutoCAD's perspective – this presents us with a really interesting optimization opportunity: rather than downloading and processing the files sequentially via synchronous API calls, we can choose to perform this work asynchronously. Asynchronous programming is a hot topic, right now, and currently a key benefit F# brings over VB.NET and C# – hence the somewhat provocative title for this class. That said, Anders Hejlsberg's recent announcement at PDC 2010 regarding the addition of asynchronous programming support to VB.NET and C# – which can be used right now with the Visual Studio Async CTP – will definitely enable this kind of functionality from your preferred .NET language, over time. Also worth checking out is the accompanying Channel 9 interview.

Anders' demo shows very eloquently why the current mode of making asynchronous calls from VB.NET and C# is inadequate. While the Parallel Extensions for .NET – provided in .NET 4.0 – was a great addition for simplifying the management of multiple tasks, strong support for asynchronous calls was missing. This area is still a key advantage of using F#: its elegant Asynchronous Workflows feature makes the description and execution of asynchronous tasks really easy.

Before we look at applying Asynchronous Workflows to this problem, let's see a standard synchronous approach in C#:

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using System.Net;

using System.IO;

using System;

 

namespace PhotosynthProcSyncCs

{

  public class PointCloudProcessor

  {

    Editor _ed;

    ProgressMeter _pm;

    string _localPath;

 

    public PointCloudProcessor(

      Editor ed, ProgressMeter pm, string localPath

    )

    {

      _ed = ed;

      _pm = pm;

      _localPath = localPath;

 
0;  }

 

    public long ProcessPointCloud(

      string path, int[] dims, string txtPath

    )

    {

      // Counter for the total number of points

 

      long totalPoints = 0;

 

      // Create our intermediate text file in the temp folder

 

      FileInfo t = new FileInfo(txtPath);

      StreamWriter sw = t.CreateText();

      using (sw)

      {

        // We'll use a web client to download each .bin file

 

        WebClient wc = new WebClient();

        using (wc)

        {

          for (int maj=0; maj < dims.Length; maj++)

          {

            for (int min=0; min < dims[maj]; min++)

            {

              // Loop for each .bin file

 

              string root =

                maj.ToString() + "_" + min.ToString() + ".bin";

              string src = path + root;

              string loc = _localPath + root;

 

              try

              {

                wc.DownloadFile(src, loc);

              }

              catch

              {

                return 0;

              }

 

              if (File.Exists(loc))

              {

  
0;            
// Open our binary file for reading

 

                BinaryReader br =

                  new BinaryReader(

                    File.Open(loc, FileMode.Open)

                  );

                using (br)

                {

                  try

                  {

                    // First information is the file version

                    // (for now we support version 1.0 only)

 

                    ushort majVer = ReadBigEndianShort(br);

                    ushort minVer = ReadBigEndianShort(br);

 

                    if (majVer != 1 || minVer != 0)

                    {

                      _ed.WriteMessage(

                        "\nCannot read a Photosynth point cloud " +

                        "of this version ({0}.{1}).",

                        majVer, minVer

                      );

                      return 0;

                    }

 

                    // Clear some header bytes we don't care about

 

                    int n = ReadCompressedInt(br);

                    for (int i = 0; i < n; i++)

                    {

                      int m = ReadCompressedInt(br);

 

                      for (int j = 0; j < m; j++)

                      {

               &#
160;        ReadCompressedInt(br);

                        ReadCompressedInt(br);

                      }

                    }

 

                    // Find out the number of points in the file

 

                    int numPoints = ReadCompressedInt(br);

                    totalPoints += numPoints;

 

                    _ed.WriteMessage(

                    "\nProcessed points_{0} containing {1} points.",

                      root, numPoints

                    );

 

                    for (int k = 0; k < numPoints; k++)

                    {

                      // Read our coordinates

 

                      float x = ReadBigEndianFloat(br);

                      float y = ReadBigEndianFloat(br);

                      float z = ReadBigEndianFloat(br);

 

                      // Read and extract our RGB values

 

                      UInt16 rgb = ReadBigEndianShort(br);

 

                      int r = (rgb >> 11) * 255 / 31;

                      int g = ((rgb >> 5) & 63) * 255 / 63;

                      int b = (rgb & 31) * 255 / 31;

 

                      // Write the point with its color to file

 

                      sw.WriteLine(

                        "{0},{1},{2},{3},{4},{5}", x, y, z, r, g, b

                      );

                    }

                  }

                  catch (System.Exception ex)

                 
{

                    _ed.WriteMessage(

                    "\nError processing point cloud file " +

                    "\"points_{0}\": {1}",

                    root, ex.Message

                    );

                  }

                }

 

                // Delete our local .bin file

 

                File.Delete(loc);

 

                // Show some progress

 

                _pm.MeterProgress();

                System.Windows.Forms.Application.DoEvents();

              }

            }

          }

        }

      }

      return totalPoints;

    }

 

    private static int ReadCompressedInt(BinaryReader br)

    {

      int i = 0;

      byte b;

 

      do

      {

        b = br.ReadByte();

        i = (i << 7) | (b & 127);

      }

      while (b < 128);

 

      return i;

    }

 

    private static float ReadBigEndianFloat(BinaryReader br)

    {

      byte[] b = br.ReadBytes(4);

      return BitConverter.ToSingle(

        new byte[] { b[3], b[2], b[1], b[0] },

        0

      );

    }

 

    private static UInt16 ReadBigEndianShort(BinaryReader br)

    {

      byte b1 = br.ReadByte();

      byte b2 = br.ReadByte();

 

      return (ushort)(b2 | (b1 << 8));

    }

  }

}

The important thing to note about the signature of the ProcessPointCloud() function – which I've kept the same across the C# and F# implementations – is the way the dims variable works: this is a simple array – populated by querying the Photosynth web service – of the number of files to download for the various point clouds in the synth. For example: if a synth contains three point clouds, the first comprising 5 files, the second of 3 files and the third of 1 file, { 5, 3, 1 } would get passed into the function, which would then attempt to download the following files: points_0_0.bin, points_0_1.bin, points_0_2.bin, points_0_3.bin, points_0_4.bin, points_1_0.bin, points_1_1.bin, points_1_2.bin and points_2_0.bin. As mentioned earlier, I've recently change the approach to only download the first point cloud for each synth, so only the points_0_x.bin files will be downloaded and processed. Which means dims will now always only have one entry.

The above code works in a very linear fashion: download a file, process it, download the next, process that, etc.

Now let's take a look at the equivalent Asynchronous Workflows implementation in F#:

module PhotosynthProcAsyncFs

 

open System.Globalization

open System.Threading

open System.Text

open System.Net

open System.IO

open System

 

// We need the SynchronizationContext of the UI thread,

// to allow us to make sure our UI update events get

// processed correctly in the calling application

 

let mutable syncContext : SynchronizationContext = null

 

// Asynchronous Worker courtesy of Don Syme:

//  http://blogs.msdn.com/dsyme/archive/2010/01/10/async-and-

//  parallel-design-patterns-in-f-reporting-progress-with-

//  events-plus-twitter-sample.aspx

 

type Agent<'T> = MailboxProcessor<'T>

 

type SynchronizationContext with

 

  // A standard helper extension method to raise an event on

  // the GUI thread

 

  member syncContext.RaiseEvent (event: Event<_>) args =

    syncContext.Post((fun _ -> event.Trigger args),state=null)

 

type AsyncWorker<'T>(jobs: seq<Async<'T>>) =

 

  // Each of these lines declares an F# event that we can raise

 

  let allCompleted  = new Event<'T[]>()

  let error        = new Event<System.Exception>()

  let canceled      = new Event<System.OperationCanceledException>()

  let jobCompleted  = new Event<int * 'T>()

 

  let cancellationCapability = new CancellationTokenSource()

 

  // Start an instance of the work

 

  member x.Start() =                                                     

 

    // Capture the synchronization context to allow us to raise

    // events back on the GUI thread

 

    if syncContext = null then

      syncContext <- SynchronizationContext.Current

 

    if syncContext = null then

      raise(

        System.NullReferenceException(

          "Synchronization context is null."))     

 

    // Mark up the jobs with numbers

 

    let jobs = jobs |> Seq.mapi (fun i job -> (job,i+1))

 

    let work =

      Async.Parallel

      [ for (job,jobNumber) in jobs ->

          async { let! result = job

                  syncContext.RaiseEvent

                    jobCompleted (jobNumber,result)

                  return result } ]

 

    Async.StartWithContinuations(

      work,

      (fun res -> syncContext.RaiseEvent allCompleted res),

      (fun exn -> syncContext.RaiseEvent error exn),

      (fun exn -> syncContext.RaiseEvent canceled exn ),

      cancellationCapability.Token)

 

  member x.CancelAsync() =

    cancellationCapability.Cancel()

 

  // Raised when a particular job completes

 

  member x.JobCompleted = jobCompleted.Publish

 

  // Raised when all jobs complete

 

  member x.AllCompleted = allCompleted.Publish

 

  // Raised when the composition is cancelled successfully

 

  member x.Canceled = canceled.Publish

 

  // Raised when the composition exhibits an error

 

  member x.Error = error.Publish

 

type PointCloudProcessor() =

 

  // Mutable state to track progress and results

 

  let mutable jobsComplete = 0

  let mutable jobsFailed = 0

  let mutable totalJobs = 0

  let mutable totalPoints = 0

  let mutable completed = false

 

  // Event to allow caller to update the UI

 

  let jobCompleted  = new Event<string * int>()

 

  // Function to access a stream asynchronously

 

  let httpAsync(url:string) =

 

    async {

      let req = WebRequest.Create(url)

      let! rsp = req.AsyncGetResponse()

      return rsp.GetResponseStream()

    }

 

  // Functions to read data from our point stream

 

  let rec readCompressedInt (i:int) (br:BinaryReader) =

    let b = br.ReadByte()

    let i = (i <<< 7) ||| ((int)b &&& 127)

    if (int)b < 128 then

      readCompressedInt i br

    else

      i

 

  let readBigEndianFloat (br:BinaryReader) =

    let b = br.ReadBytes(4)

    BitConverter.ToSingle( [| b.[3]; b.[2]; b.[1]; b.[0] |], 0)

 

  let readBigEndianShort (br:BinaryReader) =

    let b1 = br.ReadByte()

    let b2 = br.ReadByte()

    ((uint16)b2 ||| ((uint16)b1 <<< 8))

 

  // Recursive function to read n points from our stream

  // (We use an accumulator variable to enable tail-call

  // optimization)

 

  let rec readPoints acc n br =

 

    if n <= 0 then

      acc

    else

 

      // Read our coordinates

 

      let x = readBigEndianFloat br

      let y = readBigEndianFloat br

      let z = readBigEndianFloat br

 

      // Read and extract our RGB values

 

      let rgb = readBigEndianShort br

 

      let r = (rgb >>> 11) * 255us / 31us

      let g = ((rgb >>> 5) &&& 63us) * 255us / 63us

      let b = (rgb &&& 31us) * 255us / 31us

 

      readPoints ((x,y,z,r,g,b) :: acc) (n-1) br

 

  // Function to extract the various point information

  // from a stream corresponding to a single point file

 

  let extractPoints br =

 

    // First information is the file version

    // (for now we support version 1.0 only)

 

    let majVer = readBigEndianShort br

    let minVer = readBigEndianShort br

 

    if (int)majVer <> 1 || (int)minVer <> 0 then

      []

    else

 

      // Clear some header bytes we don't care about

 

      let n = readCompressedInt 0 br

      for i in 0..(int)n-1 do

        let m = readCompressedInt 0 br

        for j in 0..(int)m-1 do

          readCompressedInt 0 br |> ignore

          readCompressedInt 0 br |> ignore

 

      // Find out the number of points in the file

 

      let npts = readCompressedInt 0 br

 

      // Read and return the points

 

      readPoints [] npts br


0;

  // Recursive function to create a string from a list

  // of points. Our accumulator is a StringBuilder,

  // which is the most efficient way to collate a

  // string

 

  let rec pointsToString (acc : StringBuilder) pts =

    match pts with

    | [] -> acc.ToString()

    | (x:float32,y:float32,z:float32,r,g,b) :: t ->       

      acc.AppendFormat(

        "{0},{1},{2},{3},{4},{5}\n",

        x.ToString(CultureInfo.InvariantCulture),

        y.ToString(CultureInfo.InvariantCulture),

        z.ToString(CultureInfo.InvariantCulture),

        r,g,b)

        |> ignore

      pointsToString acc t

 

  // Expose an event that's subscribable from C#/VB

 

  [<CLIEvent>]

  member x.JobCompleted = jobCompleted.Publish

 

  // Property to indicate that we're done

 

  member x.IsComplete = completed

 

  // Property to find out of any fyailures

 

  member x.Failures = jobsFailed

 

  // Property to return the results

 

  member x.TotalPoints = totalPoints

 

  // Our main function to download and process the point

  // cloud(s) associated with a particular Photosynth

 

  member x.ProcessPointCloud baseUrl dims txtPath =

 

    // A local function to add the URL prefix to each file

 

    let pathToFile file = baseUrl + file

 

    // Generate our list of files from the list of dimensions

    // of the various point clouds

 

    // Each entry in dims corresponds to the number of files:

    //  dims[0] = 5 means "points_0_0.bin" .. "points_0_4.bin"

    //  dims[6] = 3 means "points_6_0.bin" .. "points_6_2.bin"

 

    let files =

      Array.mapi

        (fun i d ->

          Array.map (fun j -> sprintf "%d_%d.bin" i j) [| 0..d-1 |]

        )

        dims

        |> Array.concat

        |> List.ofArray

 

    // Set/reset mutable state

 

    totalJobs <- files.Length

    jobsComplete <- 0

 

    // Open the local, temporary text file to hold our points

 

    let t = new FileInfo(txtPath)

    let sw = t.Create()

 

    // An agent to store our points in the file...

    // Loops and receives messages, so that we ensure we don't

    // have a conflict of simultaneous writes

 

    let fileAgent =

      Agent.Start(fun inbox ->

        async { while true do

                  let! (msg : string) = inbox.Receive()

                  do! sw.AsyncWrite(Encoding.ASCII.GetBytes(msg)) })

 

    // Our basic asynchronous task to process a file, returning

    // the number of points

 

    let processFile (file:string) =

      async {

        let! stream = httpAsync file

        use reader = new BinaryReader(stream)

        let pts = extractPoints reader

        pointsToString (new StringBuilder()) pts |> fileAgent.Post

        return file, pts.Length

      }

 

    // Our jobs are a set of tasks, one for each file

 

    let jobs =

      [for file in files -> pathToFile file |> processFile]

 

    // Create our AsyncWorker for our jobs

 

    let worker = new AsyncWorker<_>(jobs)

 

    // Raise an event when each file is processed and update

    // our internal state

 

    worker.JobCompleted.Add(fun (jobNumber, (url , ptnum)) ->

      let file = url.Substring(url.LastIndexOf('/')+1)

      jobsComplete <- jobsComplete + 1

      syncContext.RaiseEvent jobCompleted (file, ptnum)

 

      // If the last job, close our temporary file

 

      if jobsComplete = totalJobs then

        sw.Close()

        sw.Dispose()

    )

 

    // Raise an event when an error occurs

 

    worker.Error.Add(fun ex ->

      jobsComplete <- jobsComplete + 1

      jobsFailed <- jobsFailed + 1

      syncContext.RaiseEvent jobCompleted ("Failed", 0)

    )

 

    // Raise an event on cancellation

 

    worker.Canceled.Add(fun ex ->

      worker.CancelAsync()

      jobsComplete <- totalJobs

      jobsFailed <- totalJobs

    )

 

    // Once we're all done, set the results as state to be

    // accessed by our calling routine

 

    worker.AllCompleted.Add(fun results ->

        totalPoints <- Array.sumBy snd results

        completed <- true)

 

    // Now start the work

 

    worker.Start()

This implementation makes use of the AsyncWorker<> class: a standard design pattern implemented by Don Syme to report progress during a series of asynchronous tasks. We have events – raised on the UI thread, which means we can call back into AutoCAD safely – for when tasks complete, fail or get cancelled. Let's take a closer look at the core function in this implementation, that which processes a single file:

    1 let processFile (file:string) =

    2   async {

    3     let! stream = httpAsync file

    4     use reader = new BinaryReader(stream)

    5     let pts = extractPoints reader

    6     pointsToString (new StringBuilder()) pts |> fileAgent.Post

    7     return file, pts.Length

    8   }

The first line names the function and declares it to take a string argument. The second line says the whole operation is to be considered an asynchronous task, which means it can be executed in a non-blocking way. The third line is the only actual call that is made asynchronously, as denoted by the "!", which tells the F# compiler to call the function but only continue with the assignment and the code following it once the results arrive. Th
e rest of the code actually just takes the downloaded file and processes its contents by using a BinaryReader and extracting the various points in a very linear fashion. There was really no advantage to be had in attempting to make the processing asynchronous – the real benefit is derived from performing the download asynchronously rather than the processing.

One important point about the way this code works: it's all very well requesting data which then gets processed in a random order as it arrives back, but we do need to coordinate placing that data into our text file (which, as you saw in the process diagram, will then get processed into a LAS file before that, in turn, gets indexed into a PCG and attached inside an AutoCAD drawing). The points can come in any order – we're not fussy about the ordering – but they do need to all make it in there. The way I've approached that is to use a agent-based message-passing (as described in more detail in this blog post). This sets up a central mailbox for requests to write to out text file: as the messages get processed, the points get added to the file.

What's important to note about these "tasks": we have spent all out time saying what needs to happen – not when. The timing of the execution of these tasks is handled completely by the F# runtime and the .NET Framework – we really don't need to care about how and when things happen.

Just to make sure there wasn't any inherent benefit from using F# rather than C#, I also implemented an additional "F# synchronous" mode. The code is provided in the project associated with this class.

What's potentially of more interest is the approach I've used to switch between the various implementations: I've used a capability in AutoCAD 2011 allowing system variables to be added via the Registry. Here's the .reg file I've used to do this:

Windows Registry Editor Version 5.00

 

[HKEY_LOCAL_MACHINE\SOFTWARE\Autodesk\AutoCAD\R18.1\ACAD-9001:409\Variables\BROWSEPSSYNC]

"StorageType"=dword:00000002

"LowerBound"=dword:00000000

"UpperBound"=dword:00000002

"PrimaryType"=dword:0000138b

@="0"

 

[HKEY_LOCAL_MACHINE\SOFTWARE\Autodesk\AutoCAD\R18.1\ACAD-9001:409\Variables\BROWSEPSLOG]

"StorageType"=dword:00000002

"LowerBound"=dword:00000000

"UpperBound"=dword:00000001

"PrimaryType"=dword:0000138b

@="0"

 

This adds two sysvars to AutoCAD:

  • BROWSEPSSYNC – an integer between 0 and 2, stored per-user
    • Used to indicate the synchrony mode:
      • 0 = C# synchronous
      • 1 = F# synchronous
      • 2 = F# asynchronous
  • BROWSEPSLOG – an integer between 0 and 1, stored per-user
    • Used to indicate whether to save the performance information in a log file

We're then able to get the values of these system variables using (for instance) Application.GetSystemVariable("BROWSEPSSYNC") in our code. The IMPORTPS implementation now uses the appropriate user-selected synchrony mode, and – optionally – stores the performance data in "Documents\Photosynth Point Clouds\log.txt".

To put the code through its paces, I modified the mode before importing each of a number of my favourite synths, in increasing order of size:

Vietnam Memorial Statue using C# synchronous: 46293 points from 10 files in 00:00:06.7260000

Vietnam Memorial Statue using F# synchronous: 46293 points from 10 files in 00:00:02.3120000

Vietnam Memorial Statue using F# asynchronous: 46293 points from 10 files in 00:00:01.5560000

National Geographic - Sphinx using C# synchronous: 102219 points from 21 files in 00:00:17.3470000

National Geographic - Sphinx using F# synchronous: 102219 points from 21 files in 00:00:16.5290000

National Geographic - Sphinx using F# asynchronous: 102219 points from 21 files in 00:00:04.2920000

L'epee de la Tene using C# synchronous: 405998 points from 82 files in 00:00:57.8160000

L'epee de la Tene using F# synchronous: 405998 points from 82 files in 00:00:54.2560000

L'epee de la Tene using F# asynchronous: 405998 points from 82 files in 00:00:08.3740000

Another Tres Yonis Synth using C# synchronous: 1141257 points from 229 files in 00:03:56.0490000

Another Tres Yonis Synth using F# synchronous: 1141257 points from 229 files in 00:04:20.4900000

Another Tres Yonis Synth using F# asynchronous: 1141257 points from 229 files in 00:00:21.3790000

Just to be clear: I did change the order of the execution – to make sure "C# synchronous" didn't have the disadvantage of pulling down data that was cached for the other modes – but I've reordered them for ease of reading (it didn't change anything at all in terms of results).

Here's the data in a graphical form:

Performance graph We can see that the difference in performance between C# and F# when working synchronously is modest: F# seems a bit quicker overall by then C# was much a bit quicker on the largest. But both were blown away by F# asynchronous: at worst F# async was 4 times faster, but at best it was 12 times faster!

Leave a Reply

Your email address will not be published. Required fields are marked *