October 2014

Sun Mon Tue Wed Thu Fri Sat
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  










« Coming to AU 2008? Vote to come along and let us know what you need from the AutoCAD .NET API! | Main | A video introduction »

October 27, 2008

AU Handouts: AutoCAD® .NET - Developing for AutoCAD® Using F# - Part 2

This post continues on from Part 1 of this series. You'll find much of this content has been used before in these previous posts, although post does include content updated for F# 1.9.6.2 (the September 2008 CTP).

The first thing we need to do is – as with any AutoCAD .NET project – add project references to AutoCAD’s managed assemblies, acmgd.dll and acdbmgd.dll. With F#’s integration into Visual Studio 2008 you do this in exactly the same way as you would for a C# or VB.NET project, by selecting Project -> Add Reference... from the pull-down menu or right-clicking the project inside the Solution Explorer and selecting Add Reference... from the context menu.

Here you then browse to the AutoCAD 2009 folder and filter for *mgd* files (at least this is the way I do it), and select the two we want:

Add project references to AutoCAD's managed assemblies

Figure 10 – Adding project references to AutoCAD’s managed assemblies

Now we need to make sure AutoCAD recognizes a module within a namespace, from which it is able to load commands. I found – by using .NET Reflector – that the appropriate structure is to declare your functions as the contents of a module (this needs to come after the #light directive):

module MyNamespace.MyApplication

Next we’re going to specify the .NET namespaces we’ll be using inside this application:

open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

We’ll then skip past our definitions of words and sortedWords, and define our command function:

[<CommandMethod("Words")>]

let listWords () =

  // Let's get the usual helpful AutoCAD objects

  let doc =

    Application.DocumentManager.MdiActiveDocument

  let ed = doc.Editor

  let db = doc.Database

  // "use" has the same effect as "using" in C#

  use tr =

    db.TransactionManager.StartTransaction();

  // Get appropriately-typed BlockTable and BTRs

  let bt =

    tr.GetObject

      (db.BlockTableId,OpenMode.ForRead)

    :?> BlockTable

  let ms =

    tr.GetObject

      (bt.[BlockTableRecord.ModelSpace],

       OpenMode.ForRead)

    :?> BlockTableRecord

  let ps =

    tr.GetObject

      (bt.[BlockTableRecord.PaperSpace],

       OpenMode.ForRead)

    :?> BlockTableRecord

Most of this section should be familiar to anyone who has used the .NET API to AutoCAD – there are really only a couple of ideas that may need explanation:

  1. The use keyword is just like C#’s using – but we don’t use curly braces to define scope. The scope gets defined as the remainder of the function in which the use statement has been used. Once the function completes the used object will be disposed of automatically.
  2. We’re using the dynamic cast operator (:?>) to specify the type of object we’re opening with GetObject(). This operator involves a query to check whether this is valid – if we wanted to do a static cast we could use :> instead.

Now we have opened our modelspace and paperspace objects (we could go further and open other layouts, but – once again – I’ll leave that as a follow-on exercise for those who feel the need to do it :-) we can look at the code we need to extract the text from our database-resident objects.

Let’s start by defining a local function which takes an ObjectId and uses it to open an object, and for a textual object (DBText or MText) it will return its contents:

  // A function that accepts an ObjectId and returns

  // a list of the text contents, or an empty list.

  // Note the valid use of tr, as it is in scope

  let extractText x =

    let obj = tr.GetObject(x,OpenMode.ForRead)

    match obj with

    | :? MText as m -> m.Contents

    | :? DBText as d -> d.TextString

    | _ -> ""

Once again we haven’t specified the type of the argument – this will be inferred by the system – but we could very easily do so. We’re using the transaction previously started in the listWords function – the reason for defining extractText local to it – which is quite valid, as it’s in scope.

After opening the object for read from its ID we’re using pattern-matching – a technique that is a huge timesaver for functional programmers – to check on the type of the object and return the appropriate property of it. This is just like a much cleaner switch statement in C#.

We could choose to match against any property of the object, but in our case we want to check the type, so use this operator: :?. The as keyword is a syntactic shortcut that defines a value we can then use to easily dereference the object and get at its properties and methods.

The final clause of the three is a wildcard: it will match all object that are not DBText or MText objects and return an empty string.
Now that we can get at the contents of our text objects, let’s write a quick recursive function to display the contents of the final list of words inside AutoCAD:

  // A recursive function to print the contents of a list

  let rec printList x =

    match x with

    | h :: t -> ed.WriteMessage("\n" + h); printList t

    | [] -> ed.WriteMessage("\n")

Once again, this is a local function, so using our Editor (accessed via the ed value) is quite valid. We’re using pattern-matching again to create a recursive function (indicated via the rec keyword and then the recursive call to printList). When we find an empty list ([]) we simply print a newline, but when we find a list with a head (h) and a tail (t – which may well end up being empty, by the way, we’ll find out the next time we recurse into printList), we print the head and recurse with the tail.

One thing to look out for when defining recursive functions: they really need to be defined as tail-recursive, which means that the recursive call should be the last operation. This allows the compiler to perform tail call optimization, which replaces the declared recursion with a simply while loop inside the generated code.

Why does this matter? Well, calling a function does have some overhead, as stack space is required to store information about the function and its arguments, so if we have a list of 10,000 words to print and the function hasn’t been optimized, the recursion could cause problems. (The number could be 100,000 or 1,000,000, but the point is there is a number).

The above code does get optimized properly (even if the pattern for the empty list comes after the recursive call – it’s really about the position of the recursive call in the clause that recurses, rather than the overall program), and this is easy to check with .NET Reflector. In fact there’s an article on my blog covering just this:

http://through-the-interface.typepad.com/through_the_interface/2008/02/using-reflector.html

  Seq.to_list (Seq.cast ms) @    // Create a list of modelspace ids,

    Seq.to_list (Seq.cast ps) |> //  appending those from paperspace

    List.map extractText |>      // Extract the text from each object

    sortedWords |>               // Get a sorted, canonical list of words

    printList                    // Print the resultant words

A couple of comments on the above plumbing: ms and ps are both IEnumerable types (which correspond to the Seq class in F#), but are both untyped. This means we have to cast them, to be able to access them properly from F#, and then we can simply call Seq.to_list to get the contents into a list. The @ operator appends the list of ObjectIds of objects in modelspace with those in paperspace, and we then pipe the list into a call to List.map which runs our extractText function on all the objects in the combined list. The results get piped into our sortedWords function, and we finally print them to the command-line using our recursive printList function.

Finally, we’re just going to call commit on our transaction object, as for performance reasons this is currently best practice:

  // As usual, committing is cheaper than aborting

  tr.Commit()

That’s it for our first AutoCAD application. Let’s see the entire listing:

#light

// Declare a specific namespace and module name

module MyNamespace.MyApplication

open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

// Partial application of split which can then be

// applied to a string to retrieve the contained words

let words =

  let seps = " \t~`!@#$%^&*()-=_+{}|[]\\;':\"<>?,./"

  seps.ToCharArray() |> Array.to_list |> String.split

let sortedWords x =

    List.map words x |>  // Get the words from each string

    List.concat |>       // No need for the outer list

    Set.of_list |>       // Create a set from the list

    Set.to_list          // Create a list from the set

// Now we define our command

[<CommandMethod("Words")>]

let listWords () =

  // Let's get the usual helpful AutoCAD objects

  let doc =

    Application.DocumentManager.MdiActiveDocument

  let ed = doc.Editor

  let db = doc.Database

  // "use" has the same effect as "using" in C#

  use tr =

    db.TransactionManager.StartTransaction();

  // Get appropriately-typed BlockTable and BTRs

  let bt =

    tr.GetObject

      (db.BlockTableId,OpenMode.ForRead)

    :?> BlockTable

  let ms =

    tr.GetObject

      (bt.[BlockTableRecord.ModelSpace],

       OpenMode.ForRead)

    :?> BlockTableRecord

  let ps =

    tr.GetObject

      (bt.[BlockTableRecord.PaperSpace],

       OpenMode.ForRead)

    :?> BlockTableRecord

  // Now the fun starts...

  // A function that accepts an ObjectId and returns

  // a list of the text contents, or an empty list.

  // Note the valid use of tr, as it is in scope

  let extractText x =

    let obj = tr.GetObject(x,OpenMode.ForRead)

    match obj with

    | :? MText as m -> m.Contents

    | :? DBText as d -> d.TextString

    | _ -> ""

  // A recursive function to print the contents of a list

  let rec printList x =

    match x with

    | h :: t -> ed.WriteMessage("\n" + h); printList t

    | [] -> ed.WriteMessage("\n")

  // And here's where we plug everything together...

  Seq.to_list (Seq.cast ms) @    // Create a list of modelspace ids,

    Seq.to_list (Seq.cast ps) |> //  appending those from paperspace

    List.map extractText |>      // Extract the text from each object

    sortedWords |>               // Get a sorted, canonical list of words

    printList                    // Print the resultant words

  // As usual, committing is cheaper than aborting

  tr.Commit()

Introducing parallel processing in AutoCAD via F# Asynchronous Workflows

As mentioned previously, pure functional code lends itself to be run on multiple computing cores in parallel. While the tools aren’t yet there to make this happen automatically – via implicit parallelization – this is a likely outcome, over the coming years. For now we have the possibility of writing code that uses explicit parallelization – where we specify the tasks we know can be executed at the same time and leave the language and runtime to take care of the coordination.

There are a couple of ways to do this, right now: the Parallel Extensions to .NET (also in CTP stage and due for inclusion in Visual Studio 2010) provide a number of parallel constructs, such as parallel versions of for and while loops. F# currently provides the capability to define and execute Asynchronous Workflows, which is what we’re going to look at now.

First, let’s take a look at a sample application that we’re going to parallelize. This sample goes through and queries, via RSS, the latest posts on a number of different blogs. It then generates AutoCAD geometry – text with a hyperlink – for each of these posts. So we turn AutoCAD into an RSS reader, for all intents and purposes.

    1 // Use lightweight F# syntax

    2

    3 #light

    4

    5 // Declare a specific namespace and module name

    6

    7 module MyNamespace.MyApplication

    8

    9 // Import managed assemblies

   10

   11 open Autodesk.AutoCAD.Runtime

   12 open Autodesk.AutoCAD.ApplicationServices

   13 open Autodesk.AutoCAD.DatabaseServices

   14 open Autodesk.AutoCAD.Geometry

   15 open System.Xml

   16 open System.IO

   17 open System.Net

   18

   19 // The RSS feeds we wish to get. The first two values are

   20 // only used if our code is not able to parse the feed's XML

   21

   22 let feeds =

   23   [ ("Through the Interface",

   24     "http://blogs.autodesk.com/through-the-interface",

   25     "http://through-the-interface.typepad.com/through_the_interface/atom.xml");

   26

   27     ("Don Syme's F# blog",

   28     "http://blogs.msdn.com/dsyme/",

   29     "http://blogs.msdn.com/dsyme/rss.xml");

   30

   31     ("Shaan Hurley's Between the Lines",

   32     "http://autodesk.blogs.com/between_the_lines",

   33     "http://autodesk.blogs.com/between_the_lines/rss.xml");

   34

   35     ("Scott Sheppard's It's Alive in the Lab",

   36     "http://blogs.autodesk.com/labs",

   37     "http://labs.blogs.com/its_alive_in_the_lab/rss.xml");

   38

   39     ("Volker Joseph's Beyond the Paper",

   40     "http://blogs.autodesk.com/beyond_the_paper",

   41     "http://dwf.blogs.com/beyond_the_paper/atom.xml") ]

   42

   43 // Fetch the contents of a web page, synchronously

   44

   45 let httpSync (url:string) =

   46   let req = WebRequest.Create(url)

   47   use resp = req.GetResponse()

   48   use stream = resp.GetResponseStream()

   49   use reader = new StreamReader(stream)

   50   reader.ReadToEnd()

   51

   52 // Load an RSS feed's contents into an XML document object

   53 // and use it to extract the titles and their links

   54 // Hopefully these always match (this could be coded more

   55 // defensively)

   56

   57 let titlesAndLinks (name, url, xml) =

   58   try

   59     let xdoc = new XmlDocument()

   60     xdoc.LoadXml(xml)

   61

   62     let titles =

   63       [ for n in xdoc.SelectNodes("//*[name()='title']")

   64           -> n.InnerText ]

   65     let links =

   66       [ for n in xdoc.SelectNodes("//*[name()='link']") ->

   67           let inn = n.InnerText

   68           if  inn.Length > 0 then

   69             inn

   70           else

   71             let href = n.Attributes.GetNamedItem("href").Value

   72             let rel = n.Attributes.GetNamedItem("rel").Value

   73             if href.Contains("feedburner") or rel.Contains("enclosure") then

   74                 ""

   75             else

   76               href ]

   77

   78     let descs =

   79       [ for n in xdoc.SelectNodes

   80           ("//*[name()='description' or name()='subtitle' or name()='summary']")

   81             -> n.InnerText ]

   82

   83     // A local function to filter out duplicate entries in

   84     // a list, maintaining their current order.

   85     // Another way would be to use:

   86     //    Set.of_list lst |> Set.to_list

   87     // but that results in a sorted (probably reordered) list.

   88

   89     let rec nub lst =

   90       match lst with

   91       | a::[] -> [a]

   92       | a::b ->

   93         if a = List.hd b then

   94           nub b

   95         else

   96           a::nub b

   97       | [] -> []

   98

   99     // Filter the links to get (hopefully) the same number

  100     // and order as the titles and descriptions

  101

  102     let real = List.filter (fun (x:string) -> x.Length > 0) 

  103     let lnks = real links |> nub

  104

  105     // Return a link to the overall blog, if we don't have

  106     // the same numbers of titles, links and descriptions

  107

  108     let lnum = List.length lnks

  109     let tnum = List.length titles

  110     let dnum = List.length descs

  111

  112     if tnum = 0 || lnum = 0 || lnum <> tnum || dnum <> tnum then

  113       [(name,url,url)]

  114     else

  115       List.zip3 titles lnks descs

  116   with _ -> []

  117

  118 // For a particular (name,url) pair,

  119 // create an AutoCAD HyperLink object

  120

  121 let hyperlink (name,url,desc) =

  122   let hl = new HyperLink()

  123   hl.Name <- url

  124   hl.Description <- desc

  125   (name, hl)

  126

  127 // Download an RSS feed and return AutoCAD HyperLinks for its posts

  128

  129 let hyperlinksSync (name, url, feed) =

  130   let xml = httpSync feed

  131   let tl = titlesAndLinks (name, url, xml)

  132   List.map hyperlink tl

  133

  134 // Now we declare our command

  135

  136 [<CommandMethod("rss")>]

  137 let createHyperlinksFromRss() =

  138

  139   let starttime = System.DateTime.Now

  140

  141   // Let's get the usual helpful AutoCAD objects

  142

  143   let doc =

  144     Application.DocumentManager.MdiActiveDocument

  145   let ed = doc.Editor

  146   let db = doc.Database

  147

  148   // "use" has the same effect as "using" in C#

  149

  150   use tr =

  151     db.TransactionManager.StartTransaction()

  152

  153   // Get appropriately-typed BlockTable and BTRs

  154

  155   let bt =

  156     tr.GetObject

  157       (db.BlockTableId,OpenMode.ForRead)

  158     :?> BlockTable

  159   let ms =

  160     tr.GetObject

  161       (bt.[BlockTableRecord.ModelSpace],

  162        OpenMode.ForWrite)

  163     :?> BlockTableRecord

  164

  165   // Add text objects linking to the provided list of

  166   // HyperLinks, starting at the specified location

  167

  168   // Note the valid use of tr and ms, as they are in scope

  169

  170   let addTextObjects (pt : Point3d) lst =

  171     // Use a for loop, as we care about the index to

  172     // position the various text items

  173

  174     let len = List.length lst

  175     for index = 0 to len - 1 do

  176       let txt = new DBText()

  177       let (name:string,hl:HyperLink) = List.nth lst index

  178       txt.TextString <- name

  179       let offset =

  180         if index = 0 then

  181           0.0

  182         else

  183           1.0

  184

  185       // This is where you can adjust:

  186       //  the initial outdent (x value)

  187       //  and the line spacing (y value)

  188

  189       let vec =

  190         new Vector3d

  191           (1.0 * offset,

  192            -0.5 * (Int32.to_float index),

  193            0.0)

  194       let pt2 = pt + vec

  195       txt.Position <- pt2

  196       ms.AppendEntity(txt) |> ignore

  197       tr.AddNewlyCreatedDBObject(txt,true)

  198       txt.Hyperlinks.Add(hl) |> ignore

  199

  200   // Here's where we use the varous functions

  201   // we've defined

  202

  203   let links =

  204     List.map hyperlinksSync feeds

  205

  206   // Add the resulting objects to the model-space 

  207

  208   let len = List.length links

  209   for index = 0 to len - 1 do

  210

  211     // This is where you can adjust:

  212     //  the column spacing (x value)

  213     //  the vertical offset from origin (y axis)

  214

  215     let pt =

  216       new Point3d

  217         (15.0 * (Int32.to_float index),

  218         30.0,

  219         0.0)

  220     addTextObjects pt (List.nth links index)

  221

  222   tr.Commit()

  223

  224   let elapsed =

  225       System.DateTime.op_Subtraction(System.DateTime.Now, starttime)

  226

  227   ed.WriteMessage("\nElapsed time: " + elapsed.ToString())

I have numbered the lines, to make it easier for us to talk about the changes that are needed to introduce parallelism into this sample. Both synchronous and asynchronous versions of this application are available on my blog.

I won’t go through the above code in detail, here: firstly, it’s not intended as a perfect implementation of an RSS consumer – there are too many variations in the way RSS is implemented by different sites, so I know for a fact that this code will not work for certain blogs – it’s really intended to be an example of a – potentially time-consuming – asynchronous (in this case network-based) activity that is easy to run in parallel.

A word of caution: AutoCAD is not thread-safe – it is very much a single-threaded application – so we need to coordinate the results of these tasks prior to making the changes to the AutoCAD database. Luckily F# makes this very easy for us to do, so that’s really not a problem.

Here is the updated source that makes use of Asynchronous Workflows, with the modified/new lines highlighted in red (with a grey background for those reading this in black & white :-):

    1 // Use lightweight F# syntax

    2

    3 #light

    4

    5 // Declare a specific namespace and module name

    6

    7 module MyNamespace.MyApplicationAsync

    8

    9 // Import managed assemblies

   10

   11 open Autodesk.AutoCAD.Runtime

   12 open Autodesk.AutoCAD.ApplicationServices

   13 open Autodesk.AutoCAD.DatabaseServices

   14 open Autodesk.AutoCAD.Geometry

   15 open System.Xml

   16 open System.IO

   17 open System.Net

   18

   19 // The RSS feeds we wish to get. The first two values are

   20 // only used if our code is not able to parse the feed's XML

   21

   22 let feeds =

   23   [ ("Through the Interface",

   24     "http://blogs.autodesk.com/through-the-interface",

   25     "http://through-the-interface.typepad.com/through_the_interface/atom.xml");

   26

   27     ("Don Syme's F# blog",

   28     "http://blogs.msdn.com/dsyme/",

   29     "http://blogs.msdn.com/dsyme/rss.xml");

   30

   31     ("Shaan Hurley's Between the Lines",

   32     "http://autodesk.blogs.com/between_the_lines",

   33     "http://autodesk.blogs.com/between_the_lines/rss.xml");

   34

   35     ("Scott Sheppard's It's Alive in the Lab",

   36     "http://blogs.autodesk.com/labs",

   37     "http://labs.blogs.com/its_alive_in_the_lab/rss.xml");

   38

   39     ("Volker Joseph's Beyond the Paper",

   40     "http://blogs.autodesk.com/beyond_the_paper",

   41     "http://dwf.blogs.com/beyond_the_paper/atom.xml") ]

   42

   43 // Fetch the contents of a web page, asynchronously

   44

   45 let httpAsync(url:string) =

   46   async { let req = WebRequest.Create(url)

   47           use! resp = req.GetResponseAsync()

   48           use stream = resp.GetResponseStream()

   49           use reader = new StreamReader(stream)

   50           return reader.ReadToEnd() }

   51

   52 // Load an RSS feed's contents into an XML document object

   53 // and use it to extract the titles and their links

   54 // Hopefully these always match (this could be coded more

   55 // defensively)

   56

   57 let titlesAndLinks (name, url, xml) =

   58   try

   59     let xdoc = new XmlDocument()

   60     xdoc.LoadXml(xml)

   61

   62     let titles =

   63       [ for n in xdoc.SelectNodes("//*[name()='title']")

   64           -> n.InnerText ]

   65     let links =

   66       [ for n in xdoc.SelectNodes("//*[name()='link']") ->

   67           let inn = n.InnerText

   68           if  inn.Length > 0 then

   69             inn

   70           else

   71             let href = n.Attributes.GetNamedItem("href").Value

   72             let rel = n.Attributes.GetNamedItem("rel").Value

   73             if href.Contains("feedburner") or rel.Contains("enclosure") then

   74                 ""

   75             else

   76               href ]

   77

   78     let descs =

   79       [ for n in xdoc.SelectNodes

   80           ("//*[name()='description' or name()='subtitle' or name()='summary']")

   81             -> n.InnerText ]

   82

   83     // A local function to filter out duplicate entries in

   84     // a list, maintaining their current order.

   85     // Another way would be to use:

   86     //    Set.of_list lst |> Set.to_list

   87     // but that results in a sorted (probably reordered) list.

   88

   89     let rec nub lst =

   90       match lst with

   91       | a::[] -> [a]

   92       | a::b ->

   93         if a = List.hd b then

   94           nub b

   95         else

   96           a::nub b

   97       | [] -> []

   98

   99     // Filter the links to get (hopefully) the same number

  100     // and order as the titles and descriptions

  101

  102     let real = List.filter (fun (x:string) -> x.Length > 0) 

  103     let lnks = real links |> nub

  104

  105     // Return a link to the overall blog, if we don't have

  106     // the same numbers of titles, links and descriptions

  107

  108     let lnum = List.length lnks

  109     let tnum = List.length titles

  110     let dnum = List.length descs

  111

  112     if tnum = 0 || lnum = 0 || lnum <> tnum || dnum <> tnum then

  113       [(name,url,url)]

  114     else

  115       List.zip3 titles lnks descs

  116   with _ -> []

  117

  118 // For a particular (name,url) pair,

  119 // create an AutoCAD HyperLink object

  120

  121 let hyperlink (name,url,desc) =

  122   let hl = new HyperLink()

  123   hl.Name <- url

  124   hl.Description <- desc

  125   (name, hl)

  126

  127 // Use asynchronous workflows in F# to download

  128 // an RSS feed and return AutoCAD HyperLinks

  129 // corresponding to its posts

  130

  131 let hyperlinksAsync (name, url, feed) =

  132   async { let! xml = httpAsync feed

  133           let tl = titlesAndLinks (name, url, xml)

  134           return List.map hyperlink tl }

  135

  136 // Now we declare our command

  137

  138 [<CommandMethod("arss")>]

  139 let createHyperlinksFromRssAsync() =

  140

  141   let starttime = System.DateTime.Now

  142

  143   // Let's get the usual helpful AutoCAD objects

  144

  145   let doc =

  146     Application.DocumentManager.MdiActiveDocument

  147   let ed = doc.Editor

  148   let db = doc.Database

  149

  150   // "use" has the same effect as "using" in C#

  151

  152   use tr =

  153     db.TransactionManager.StartTransaction()

  154

  155   // Get appropriately-typed BlockTable and BTRs

  156

  157   let bt =

  158     tr.GetObject

  159       (db.BlockTableId,OpenMode.ForRead)

  160     :?> BlockTable

  161   let ms =

  162     tr.GetObject

  163       (bt.[BlockTableRecord.ModelSpace],

  164        OpenMode.ForWrite)

  165     :?> BlockTableRecord

  166

  167   // Add text objects linking to the provided list of

  168   // HyperLinks, starting at the specified location

  169

  170   // Note the valid use of tr and ms, as they are in scope

  171

  172   let addTextObjects (pt : Point3d) lst =

  173     // Use a for loop, as we care about the index to

  174     // position the various text items

  175

  176     let len = List.length lst

  177     for index = 0 to len - 1 do

  178       let txt = new DBText()

  179       let (name:string,hl:HyperLink) = List.nth lst index

  180       txt.TextString <- name

  181       let offset =

  182         if index = 0 then

  183           0.0

  184         else

  185           1.0

  186

  187       // This is where you can adjust:

  188       //  the initial outdent (x value)

  189       //  and the line spacing (y value)

  190

  191       let vec =

  192         new Vector3d

  193           (1.0 * offset,

  194            -0.5 * (Int32.to_float index),

  195            0.0)

  196       let pt2 = pt + vec

  197       txt.Position <- pt2

  198       ms.AppendEntity(txt) |> ignore

  199       tr.AddNewlyCreatedDBObject(txt,true)

  200       txt.Hyperlinks.Add(hl) |> ignore

  201

  202   // Here's where we do the real work, by firing

  203   // off - and coordinating - asynchronous tasks

  204   // to create HyperLink objects for all our posts

  205

  206   let links =

  207     Async.Run

  208       (Async.Parallel

  209         [ for (name,url,feed) in feeds ->

  210           hyperlinksAsync (name,url,feed) ])

  211     |> Array.to_list

  212

  213   // Add the resulting objects to the model-space 

  214

  215   let len = List.length links

  216   for index = 0 to len - 1 do

  217

  218     // This is where you can adjust:

  219     //  the column spacing (x value)

  220     //  the vertical offset from origin (y axis)

  221

  222     let pt =

  223       new Point3d

  224         (15.0 * (Int32.to_float index),

  225         30.0,

  226         0.0)

  227     addTextObjects pt (List.nth links index)

  228

  229   tr.Commit()

  230

  231   let elapsed =

  232       System.DateTime.op_Subtraction(System.DateTime.Now, starttime)

  233

  234   ed.WriteMessage("\nElapsed time: " + elapsed.ToString())

Let's look at the specific changes:

  • Line 7 has been changed to allow both files to be part of the same project.
  • Lines 45-50 implement a new, asynchronous function to download content from a URL. The async primitive coordinates a set of activities, while the let! and use! statements indicate that these right-hand side of the operation will be run asynchronously and that the results should be bound to the left. So here we're only getting the HTTP content asynchronously - the reading is to occur synchronously.
  • Lines 131-134 implement an asynchronous task that not only calls our asynchronous HTTP request function but coordinates the creation of AutoCAD geometry based on the contents received.
  • Lines 207-211 are where we make use of these newly-defined functions by firing them off in parallel (the framework will use the processing capabilities available to it to execute the tasks as efficiently as possible) and coordinating the results into a single array, which we convert to a list to maintain our previous processing code.

When we run either the RSS or ARSS (its asynchronous version), we should see this kind of result:

RSS feeds inside AutoCAD

Figure 11 – AutoCAD geometry created from our RSS feeds

Now let’s see how they compare in terms of performance. I executed the RSS and ARSS commands a number of times in sequence to get a feel for relative performance.

Command: rss

Elapsed time: 00:00:08.1958195

Command: arss

Elapsed time: 00:00:02.2802280

Command: rss

Elapsed time: 00:00:04.1264126

Command: arss

Elapsed time: 00:00:03.6343634

Command: rss

Elapsed time: 00:00:03.6563656

Command: arss

Elapsed time: 00:00:01.9891989

Command: rss

Elapsed time: 00:00:03.1673167

Command: arss

Elapsed time: 00:00:03.1223122

Command: rss

Elapsed time: 00:00:05.7375737

Command: arss

Elapsed time: 00:00:01.9391939

The first execution time is much higher due to an initial startup penalty or the need to fill some page cache with the content. On average, though, the asynchronous code runs in 60-70% of the time needed by the synchronous version. The code was run a dual-core notebook: some of the performance will be related to using both cores, but most will be due to the parallelization of asynchronous tasks that have some latency due to use of the network. With more accesses in parallel you would see this performance difference become increasingly exaggerated.

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d83452464869e2010535ba8716970c

Listed below are links to weblogs that reference AU Handouts: AutoCAD® .NET - Developing for AutoCAD® Using F# - Part 2:

blog comments powered by Disqus

Feed/Share

10 Random Posts