Archives for the Month of December, 2005

30 Year Vintage Computer Collection on EBay

Somebody is selling their gigantic 30 year vintage computer collection as a single lot on EBay

At several hundred collectible computers, and nearly as many manuals, this is a really nice trip down memory lane.  Gotta get me one of those laptops!

burn-console: Optimized, and Ready For Marshmallows

In the last article, I introduced a library function to help you invoke inline C# from your scripts.  Since we carefully profiled the burn-console script’s performance, we know exactly where to apply these inline performance optimizations: to the updateBuffer function, and the updateScreen function.

 

The completed script is included below. The inline optimizations bring us to about 30 - 40 frames per second, easily fast enough to provide smooth animation.

 

 

[Script download: http://www.leeholmes.com/projects/Burn-Console/Burn-Console.ps1]

 

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
###############################################################################
## Burn-Console.ps1
##
## Create a fire effect in PowerShell, using the Console text buffer as the
## rendering surface.
##
## Great overview of the fire effect algorithm here:
## http://freespace.virgin.net/hugo.elias/models/m_fire.htm
##
###############################################################################

function main
{
    write-debug "ENTER main"
    ## Rather than a simple red fire, we'll introduce oranges and yellows
    ## by including Yellow as one of the base colours
    $colours = "Yellow","Red","DarkRed","Black"
   
    ## The four characters that we use to dither with, along with the
    ## percentage of the foreground colour that they show
    $dithering = "█","▓","▒","░"
    $ditherFactor = 1,0.75,0.5,0.25
   
    ## Hold the palette. We actually store each entry as a BufferCell,
    ## since we need to retain a foreground colour, background colour,
    ## and dithering character.
    [System.Management.Automation.Host.BufferCell[]] $palette = `
        new-object System.Management.Automation.Host.BufferCell[] 256
   
    ## Resize the console to 70, 61 so we have a consistent buffer
    ## size for performance comparison.
    $bufferSize = new-object System.Management.Automation.Host.Size 70,61
    $host.UI.RawUI.WindowSize = $bufferSize

    ## Retrieve some commonly used dimensions
    $windowWidth = $host.UI.RawUI.WindowSize.Width
    $windowHeight = $host.UI.RawUI.WindowSize.Height
    $origin = `
        new-object System.Management.Automation.Host.Coordinates 0,0
    $dimensions = `
        new-object System.Management.Automation.Host.Rectangle `
            0,0,$windowWidth,$windowHeight
   
    ## Create our random number generator
    $random = new-object Random
    $workingBuffer = new-object System.Int32[] ($windowHeight * $windowWidth)
    $screenBuffer = new-object System.Int32[] ($windowHeight * $windowWidth)

    clear-host
   
    ## Generate the palette
    generatePalette
    # displayPalette
    # return;

    ## Update the buffer, then update the screen until the user presses a key.
    ## Keep track of the total time and frames generated to let us display
    ## performance statistics.
    $frameCount = 0
    $totalTime = measure-command {
   
        while($true)
        {
            if([Console]::KeyAvailable)
            {

                $key = [Console]::ReadKey()
                if(($key.Key -eq 'Escape') -or
                    ($key.Key -eq 'Q') -or
                    ($key.Key -eq 'C'))
                {
                    break
                }
            }
       
            updateBuffer
            updateScreen
            $frameCount++
        }
    }
   
    ## Clean up and exit
    $host.UI.RawUI.ForegroundColor = "Gray"
    $host.UI.RawUI.BackgroundColor = "Black"
   
    write-host
    write-host "$($frameCount / $totalTime.TotalSeconds) frames per second."
    write-debug "EXIT"
}

## Update a back-buffer to hold all of the information we want to display on
## the screen. To do this, we first re-generate the fire pixels on the bottom
## row. With that done, we visit every pixel in the screen buffer, and figure
## out the average heat of its neighbors. Once we have that average, we move
## that average heat one pixel up.
function updateBuffer
{
    ## This function takes the most of our time, so we'll do it inline.
    ## Inputs:
    ## Window Height
    ## Window Width
    ## Screen Buffer
    ## Random Number Generator
    ## Output:
    ## Working Buffer
   
    [System.Collections.ArrayList] $inputs = `
        new-object System.Collections.ArrayList
    [void] $inputs.Add([int] $windowHeight)
    [void] $inputs.Add([int] $windowWidth)
    [void] $inputs.Add([int[]] $screenBuffer)
    [void] $inputs.Add([System.Random] $random)
   
    $code = @"
    public static Object UpdateBuffer(System.Collections.ArrayList arg)
    {
        // Unpack the inputs from our input object
        int windowHeight = (int) ((System.Collections.ArrayList) arg)[0];
        int windowWidth = (int) ((System.Collections.ArrayList) arg)[1];
        int[] screenBuffer = (int[]) ((System.Collections.ArrayList) arg)[2];
        Random random = (Random) ((System.Collections.ArrayList) arg)[3];
         
        // Start fire on the last row of the screen buffer
        for(int column = 0; column < windowWidth; column++)
        {
            // There is an 80% chance that a pixel on the bottom row will
            // start new fire.
            if(random.NextDouble() >= 0.20)
            {
                // The chosen pixel gets a random amount of heat. This gives
                // us a lot of nice colour variation.
                screenBuffer[(windowHeight - 2) * (windowWidth) + column] =
                    (int) (random.NextDouble() * 255);
            }
        }
         
        int[] tempWorkingBuffer = (int[]) screenBuffer.Clone();
         
        // Propigate the fire
        int baseOffset = windowWidth + 1;
        for(int row = 1; row < (windowHeight - 1); row++)
        {
            for(int column = 1; column < (windowWidth - 1); column++)
            {
                // Get the average colour from the four pixels surrounding
                // the current pixel
                double colour =
                    (
                        screenBuffer[baseOffset] +
                        screenBuffer[baseOffset - 1] +
                        screenBuffer[baseOffset + 1] +
                        screenBuffer[baseOffset + windowWidth]
                     ) / 4.0;
 
                // Cool it off a little. We apply uneven cooling, otherwise
                // the cool dark red tends to stretch up for too long.
                if(colour > 0)
                {
                 &#
160;  if(colour > 70)
                    {
                        colour -= 1;
                    }
                    else
                    {
                        colour -= 3;
                         
                        if(colour < 1)
                        {
                            colour = 0;
                        }
                        else if(colour < 20)
                        {
                            colour -= 1;
                        }
                    }
                }
 
                // Store the result into the previous row -- that is, one buffer
                // cell up.
                tempWorkingBuffer[baseOffset - windowWidth] = (int) colour;
                baseOffset ++;
            }
             
            baseOffset += 2;
        }
 
        return tempWorkingBuffer;
    }
"@

    $returnClass = Add-Type -MemberDefinition $code -Name BurnConsoleUtils1 -PassThru 
    $returned = $returnClass::UpdateBuffer($inputs)
    $SCRIPT:workingBuffer = $returned
}

## Take the contents of our working buffer and blit it to the screen
## We do this in one highly-efficent step (the SetBufferContents) so that
## users don't see each individial pixel get updated.
function updateScreen
{
    write-debug "ENTER updateScreen"
   
    ## This function takes up a lot of time, so we'll do it inline.
    ## Inputs:
    ## host.UI.RawUI
    ## palette
    ## workingBuffer
    ## origin
    ## dimensions
    ## windowHeight
    ## windowWidth
    ## Output:
    ## None
   
    [System.Collections.ArrayList] $inputs = `
        new-object System.Collections.ArrayList
    [void] $inputs.Add([System.Management.Automation.Host.PSHostRawUserInterface] $host.UI.RawUI)
    [void] $inputs.Add([System.Management.Automation.Host.BufferCell[]] $palette)
    [void] $inputs.Add([int[]] $workingBuffer)
    [void] $inputs.Add([System.Management.Automation.Host.Coordinates] $origin)
    [void] $inputs.Add([System.Management.Automation.Host.Rectangle] $dimensions)
    [void] $inputs.Add([int] $windowHeight)
    [void] $inputs.Add([int] $windowWidth)
   
    $code = @"
    public static void UpdateScreen(System.Collections.ArrayList arg)
    {
        System.Management.Automation.Host.PSHostRawUserInterface rawUI =
            (System.Management.Automation.Host.PSHostRawUserInterface)
                ((System.Collections.ArrayList) arg)[0];
        System.Management.Automation.Host.BufferCell[] palette =
            (System.Management.Automation.Host.BufferCell[])
                ((System.Collections.ArrayList) arg)[1];
        int[] workingBuffer =
            (int[]) ((System.Collections.ArrayList) arg)[2];
        System.Management.Automation.Host.Coordinates origin =
            (System.Management.Automation.Host.Coordinates)
                ((System.Collections.ArrayList) arg)[3];
        System.Management.Automation.Host.Rectangle dimensions =
            (System.Management.Automation.Host.Rectangle)
                ((System.Collections.ArrayList) arg)[4];
        int windowHeight = (int) ((System.Collections.ArrayList) arg)[5];
        int windowWidth = (int) ((System.Collections.ArrayList) arg)[6];
 
        // Create a working buffer to hold the next screen that we want to
        // create.
        System.Management.Automation.Host.BufferCell[,] nextScreen =
            rawUI.GetBufferContents(dimensions);
         
        // Go through our working buffer (that holds our next animation frame)
        // and place its contents into the buffer that we will soon blast into
        // the real RawUI
        for(int row = 0; row < windowHeight; row++)
        {
            for(int column = 0; column < windowWidth; column++)
            {
                nextScreen[row, column] = palette[workingBuffer[(row * windowWidth) + column]];
 &#
160;          }
        }
         
        // Bulk update the RawUI's buffer with the contents of our next screen
        rawUI.SetBufferContents(origin, nextScreen);
    }
"@

    $returnClass = Add-Type -MemberDefinition $code -Name BurnConsoleUtils2 -PassThru 
    $returnClass::UpdateScreen($inputs)

    ## And finally update our representation of the screen buffer to hold
    ## what actually is on the screen
    $SCRIPT:screenBuffer = $workingBuffer.Clone()

    write-debug "EXIT"
}

## Generates a palette of 256 colours. We create every combination of
## foreground colour, background colour, and dithering character, and then
## order them by their visual intensity.
##
## The visual intensity of a colour can be expressed by the NTSC luminance
## formula. That formula depicts the apparent brightness of a colour based on
## our eyes' sensitivity to different wavelengths that compose that colour.
## http://en.wikipedia.org/wiki/Luminance_%28video%29
function generatePalette
{
    ## The apparent intensities of our four primary colours.
    ## However, the formula under-represents the intensity of our straight
    ## red colour, so we artificially inflate it.
    $luminances = 225.93,106.245,38.272,0
    $apparentBrightnesses = @{}

    ## Cycle through each foreground, background, and dither character
    ## combination. For each combination, find the apparent intensity of the
    ## foreground, and the apparent intensity of the background. Finally,
    ## weight the contribution of each based on how much of each colour the
    ## dithering character shows.
    ## This provides an intensity range between zero and some maximum.
    ## For each apparent intensity, we store the colours and characters
    ## that create that intensity.
    $maxBrightness = 0
    for($fgColour = 0; $fgColour -lt $colours.Count; $fgColour++)
    {
        for($bgColour = 0; $bgColour -lt $colours.Count; $bgColour++)
        {
            for($ditherCharacter = 0; 
                $ditherCharacter -lt $dithering.Count; 
                $ditherCharacter++)
            {
                $apparentBrightness = `
                    $luminances[$fgColour] * $ditherFactor[$ditherCharacter] +
                    $luminances[$bgColour] *
                        (1 - $ditherFactor[$ditherCharacter])
                   
                if($apparentBrightness -gt $maxBrightness) 
                { 
                    $maxBrightness = $apparentBrightness 
                }
                   
                $apparentBrightnesses[$apparentBrightness] = `
                    "$fgColour$bgColour$ditherCharacter"
            }
       }
    }

    ## Finally, we normalize our computed intesities into a pallete of
    ## 0 to 255. If a given intensity is 30% towards our maximum intensity,
    ## then it should be in the palette at 30% of index 255.
    $paletteIndex = 0
    foreach($key in ($apparentBrightnesses.Keys | sort))
    {
        $keyValue = $apparentBrightnesses[$key]
        do
        {
            $character = $dithering[[Int32]::Parse($keyValue[2])]
            $fgColour = $colours[[Int32]::Parse($keyValue[0])]
            $bgColour = $colours[[Int32]::Parse($keyValue[1])]
           
            $bufferCell = `
                new-object System.Management.Automation.Host.BufferCell `
                    $character,
                    $fgColour,
                    $bgColour,
                    "Complete"
                   
            $palette[$paletteIndex] = $bufferCell
            $paletteIndex++
        } while(($paletteIndex / 256) -lt ($key / $maxBrightness))
    }
}

## Dump the palette to the screen.
function displayPalette
{
    for($paletteIndex = 254; $paletteIndex -ge 0; $paletteIndex--)
    {
        $bufferCell = $palette[$paletteIndex]
        $fgColor = $bufferCell.ForegroundColor
        $bgColor = $bufferCell.BackgroundColor
        $character = $bufferCell.Character

        $host.UI.RawUI.ForegroundColor = $fgColor
        $host.UI.RawUI.BackgroundColor = $bgColor
        write-host -noNewLine $character
    }
   
    write-host
}

. main

 

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]
[Edit: Updated to work with PowerShell RTM and updated Invoke-Inline.ps1 script]

Library for Inline C# in MSH

In the last post, we got nearly as far as we could in improving the performance of our MSH script.  We used the profiler to help target our performance optimizations.  After we were finished, setting variables, incrementing counters, and comparing colour values took up the vast majority of our time.  We can’t make these statements more efficient, nor can we execute them less frequently.

To really push the performance of this script, we’ll write the highly critical sections using inline C#, rather than MSH.

This interop idea is something that many people end implement on their own after playing with .Net (and MSH) for any length of time.  I use it extensively in my C# Performance Comparison tool, MOW wrote it for VB in MSH, Jeffrey Snover wrote it for C# in MSH (as did half of the rest of the Monad team,) and Bruce Payette even wrote one for MSIL in MSH.

Normally, I would just link to an implementation of “C# in MSH,” but I’m going to offer a library function that goes a bit further:

  1. The inline C# accepts dynamic arguments, and returns dynamic values.  Other implementations hard-code the parameters, and can’t interact with the rest of the script.
  2. The inlining mechanism caches the temporary compiled C# class.  No matter how often you call the same inline code in a script, you will only suffer the (relatively minor) compilation burden once.
  3. The inlined code does not require any class definitions, namespace imports, or other template code.

It is commented in excruciating detail below.  Don't worry, you'll have a nice speedy fire warming up your console in no time flat.

[Edit: A better way to get the installation path of PowerShell is $psHome]
[Edit: Updated to work with RTM builds]
[Edit: Added ability to reference other DLLs]

################################################################################ 
## Invoke-Inline.ps1
## Library support for inline C# 
## 
## Usage 
##  1) Define just the body of a C# method, and store it in a string.  "Here 
##     strings" work great for this.  The code can be simple: 
## 
##     $codeToRun = "Console.WriteLine(Math.Sqrt(337));" 
## 
##     or more complex: 
## 
##     $codeToRun = @" 
##         string firstArg = (string) ((System.Collections.ArrayList) arg)[0]; 
##         int secondArg = (int) ((System.Collections.ArrayList) arg)[1]; 
## 
##         Console.WriteLine("Hello {0} {1}", firstArg, secondArg ); 
##      
##         returnValue = secondArg * 3; 
##     "@ 
## 
##  2) (Optionally) Pack any arguments to your function into a single object. 
##     This single object should be strongly-typed, so that PowerShell does
##     not treat  it as a PsObject. 
##     An ArrayList works great for multiple elements.  If you have only one  
##     argument, you can pass it directly. 
##    
##     [System.Collectionts.ArrayList] $arguments =
##         New-Object System.Collections.ArrayList 
##     [void] $arguments.Add("World") 
##     [void] $arguments.Add(337) 
## 
##  3) Invoke the inline code, optionally retrieving the return value.  You can 
##     set the return value in your inline code by assigning it to the 
##     "returnValue" variable as shown above. 
## 
##     $result = Invoke-Inline $codeToRun $arguments 
## 
## 
##     If your code is simple enough, you can even do this entirely inline: 
## 
##     Invoke-Inline "Console.WriteLine(Math.Pow(337,2));" 
##   
################################################################################ 
param(
    [string] $code,
    [object] $arg,
    [string[]] $reference = @()
    )

## Stores a cache of generated inline objects.  If this library is dot-sourced 
## from a script, these objects go away when the script exits. 
if(-not (Test-Path Variable:\lee.holmes.inlineCache))
{
    ${GLOBAL:lee.holmes.inlineCache} = @{}
}

## The main function to execute inline C#.   
## Pass the argument to the function as a strongly-typed variable.  They will  
## be available from C# code as the Object variable, "arg". 
## Any values assigned to the "returnValue" object by the C# code will be  
## returned to MSH as a return value. 

function main
{
    ## See if the code has already been compiled and cached 
    $cachedObject = ${lee.holmes.inlineCache}[$code]

    ## The code has not been compiled or cached 
    if($cachedObject -eq $null)
    {
        $codeToCompile =
@"
    using System;

    public class InlineRunner
    {
        public Object Invoke(Object arg)
        {
            Object returnValue = null;

            $code

            return returnValue;
        }
    }
"@

        ## Obtains an ICodeCompiler from a CodeDomProvider class. 
        $provider = New-Object Microsoft.CSharp.CSharpCodeProvider

        ## Get the location for System.Management.Automation DLL 
        $dllName = [PsObject].Assembly.Location

        ## Configure the compiler parameters 
        $compilerParameters = New-Object System.CodeDom.Compiler.CompilerParameters

        $assemblies = @("System.dll", $dllName)
        $compilerParameters.ReferencedAssemblies.AddRange($assemblies)
        $compilerParameters.ReferencedAssemblies.AddRange($reference)
        $compilerParameters.IncludeDebugInformation = $true
        $compilerParameters.GenerateInMemory = $true

        ## Invokes compilation.  
        $compilerResults =
            $provider.CompileAssemblyFromSource($compilerParameters, $codeToCompile)

        ## Write any errors if generated.         
        if($compilerResults.Errors.Count -gt 0)
        {
            $errorLines = ""
            foreach($error in $compilerResults.Errors)
            {
                $errorLines += "`n`t" + $error.Line + ":`t" + $error.ErrorText
            }
            Write-Error $errorLines
        }
        ## There were no errors.  Store the resulting object in the object 
        ## cache. 
        else
        {
            ${lee.holmes.inlineCache}[$code] =
                $compilerResults.CompiledAssembly.CreateInstance("InlineRunner")
        }

        $cachedObject = ${lee.holmes.inlineCache}[$code]
   }

   ## Finally invoke the C# code 
   if($cachedObject -ne $null)
   {
       return $cachedObject.Invoke($arg)
   }
}

. Main

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Scripting with the Microsoft Shell — Now on Script Center

The scripting guys continue to impart their magic scripting touch, now with a great big dose of MSH.

Their "Scripting with the Microsoft Shell" Script Center went live recently, and it's already chock-full of sample scripts -- most applying the power of WMI interop.

Their first article covers Accessing WMI From the Microsoft Shell -- check it out!

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Accessing Performance Counters in PowerShell

The question came up on the newsgroup on how to monitor CPU usage in PowerShell.  I wrote a script to demonstrate this some time ago with the intent to write about it – so now is probably an ideal time.

The following poll-process script retrieves the process name, main window title, processor usage, disk activity, and working set.  It continually refreshes the display to give you a task manager-like experience.

One advantage it offers over Task Manager is the disk activity column.  Since total disk activity is actually measured by several separate perf counters, it uses a heuristic to combine them into a single number.

Now, when you hear something drilling holes in your hard drive, you can find out what it is.

## poll-process.ps1
## Continuously display a process list, sorted
## by the desired criteria.
##
## usage: poll-process [sortCriteria] [pollInterval]
##
## sortCriteria must be one of "Id", "ProcessName", "MainWindowTitle", 
##                              "Processor", "Disk", or "WorkingSet"
## pollInterval is specified in milliseconds
##
param(
    [string] $sortCriteria = "Processor",
    [int] $pollInterval = 750
    )

function main
{
    ## Store the performance counters we need
    ## for the CPU, and Disk I/O numbers
    $cpuPerfCounters = @{}
    $ioOtherOpsPerfCounters = @{}
    $ioOtherBytesPerfCounters = @{}
    $ioDataOpsPerfCounters = @{}
    $ioDataBytesPerfCounters = @{}
    $processes = $null
    $lastPoll = get-date
    
    $lastSnapshotCount = 0
    $lastWindowHeight = 0
    
    ## The coordinates to which we position the output
    $coords = new-object System.Management.Automation.Host.Coordinates 0,0
    clear-host

    while(-not $host.UI.RawUI.KeyAvailable)
    {
        ## Set the cursor position, get the processes, and store
        ## the time of the snapshot
        $host.UI.RawUI.CursorPosition = $coords
        $processes = get-process | sort Id

        ## Go through all of the processes we captured
        foreach($process in $processes)
        {
            ## Get the disk activity, based on I/O Perf Counters,
            ## for the process in question.  Then, add it as a note.
            $activity = get-diskActivity $process
            $process | add-member NoteProperty Disk $activity

            $cpuPercent = get-cpuPercent $process
            $process | add-member NoteProperty Processor $cpuPercent
         }

        $windowHeight = $host.Ui.RawUi.WindowSize.Height
        ## Since clear-host makes the screen flash, we only do so when
        ## we have fewer processes than we did the last time
        if(($processes.Count -lt $lastSnapshotCount) -or `
            (-not ($lastWindowHeight -eq $windowHeight)))
        { 
            clear-host 
        }
        
        ## Tailor the length of the list to the height of the 
        ## window.  If the window is to short, show no output.
        if($windowHeight -le 7
        { 
            $output = $null 
        }
        else 
        {
            $output = $processes | sort -desc $sortCriteria | `
                select-object -First ($windowHeight - 7)
        }
        
        ## Display the results
        $output | format-table Id,ProcessName,MainWindowTitle,Processor,Disk,WorkingSet
        
        if($processes.Count -gt ($windowHeight - 7))
        {
            $notDisplay
ed = ($processes.Count - $output.Count)
            "[ $notDisplayed process(es) not shown ]"
        }
        
        $lastSnapshotCount = $processes.Count
        $lastWindowHeight = $windowHeight
        
        ## Sleep for their desired amount of elapsed time,
        ## adjusted for how much time we've actually spent working.
        $elapsed = [int] (get-date).Subtract($lastPoll).TotalMilliseconds
        $lastPoll = get-date
        if($pollInterval -gt $elapsed)
        { 
            start-sleep -m ($pollInterval - $elapsed)
        }
    }
}

## As a heuristic, gets the total IO and Data operations per second, and
## returns their sum.
function get-diskActivity (
    $process = $(throw "Please specify a process for which to get disk usage.")
    )
{
    $processName = get-processName $process
    
    ## We store the performance counter objects in a hashtable.  If we don't,
    ## then they fail to return any information for a few seconds.
    if(-not $ioOtherOpsPerfCounters[$processName])
    {
        $ioOtherOpsPerfCounters[$processName] = `
            new-object System.Diagnostics.PerformanceCounter `
                "Process","IO Other Operations/sec",$processName
    }
    if(-not $ioOtherBytesPerfCounters[$processName])
    {
        $ioOtherBytesPerfCounters[$processName] = `
            new-object System.Diagnostics.PerformanceCounter `
                "Process","IO Other Bytes/sec",$processName
    }
    if(-not $ioDataOpsPerfCounters[$processName])
    {
        $ioDataOpsPerfCounters[$processName] = `
            new-object System.Diagnostics.PerformanceCounter `
                "Process","IO Data Operations/sec",$processName
    }
    if(-not $ioDataBytesPerfCounters[$processName])
    {
        $ioDataBytesPerfCounters[$processName] = `
            new-object System.Diagnostics.PerformanceCounter `
                "Process","IO Data Bytes/sec",$processName
    }

    ## If a process exits between the time we capture the processes and now,
    ## then we will be unable to get its NextValue().  This trap simply
    ## catches the error and continues.
    trap { continue; }

    ## Get the performance counter values
    $ioOther = (100 * $ioOtherOpsPerfCounters[$processName].NextValue()) + `
        ($ioOtherBytesPerfCounters[$processName].NextValue())
    $ioData = (100 * $ioDataOpsPerfCounters[$processName].NextValue()) + `
        ($ioDataBytesPerfCounters[$processName].NextValue())
    
    return [int] ($ioOther + $ioData)    
}

## Get the percentage of time spent by a process.
## Note: this is multiproc "unaware."  We need to divide the
## result by the number of processors.
function get-cpuPercent (
    $process = $(throw "Please specify a process for which to get CPU usage.")
    )
{
    $processName = get-processName $process
    
    ## We store the performance counter objects in a hashtable.  If we don't,
    ## then they fail to return any information for a few seconds.
    if(-not $cpuPerfCounters[$processName])
 
   {
        $cpuPerfCounters[$processName] = `
            new-object System.Diagnostics.PerformanceCounter `
                "Process","% Processor Time",$processName
    }

    ## If a process exits between the time we capture the processes and now,
    ## then we will be unable to get its NextValue().  This trap simply
    ## catches the error and continues.
    trap { continue; }

    ## Get the performance counter values
    $cpuTime = ($cpuPerfCounters[$processName].NextValue() / $env:NUMBER_OF_PROCESSORS)
    return [int] $cpuTime
}

## Performance counters are keyed by process name.  However,
## processes may share the same name, so duplicates are named
## <process>#1, <process>#2, etc.
function get-processName (
    $process = $(throw "Please specify a process for which to get the name.")
    )
{
    ## If a process exits between the time we capture the processes and now,
    ## then we will be unable to get its information.  This simply
    ## ignores the error.
    $errorActionPreference = "SilentlyContinue"

    $processName = $process.ProcessName
    $localProcesses = get-process -ProcessName $processName | sort Id
    
    if(@($localProcesses).Count -gt 1)
    {
        ## Determine where this one sits in the list
        $processNumber = -1
        for($counter = 0; $counter -lt $localProcesses.Count; $counter++)
        {
            if($localProcesses[$counter].Id -eq $process.Id) { break }
        }
        
        ## Append its unique identifier, if required
        $processName += "#$counter"
    }
    
    return $processName
}

. main

[Update: MOW wrote a piece about Perf Counters as well, based on the same newsgroup thread: Getting Performance Monitor Info from Monad]

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

[Edit: Script updated for PowerShell RC2]

burn-console Part III: The Most Efficient MSH Script

Now that we have our script profiler up and running, we instrument our script a little to mark regions we are concerned about.  You can download that starting script here: [burn-console-1.profiler.msh]   After running the script profiler, we get the performance breakdown:

Breakdown by line:
----------------------------
 15%: Line  123 -             if($colour -lt 20) { $colour -= 1 }
 14%: Line  122 -             if($colour -le 70) { $colour -= 3 }
 11%: Line  124 -             if($colour -lt 0) { $colour = 0 }
 10%: Line  128 -             $tempWorkingBuffer[$baseOffset -
  8%: Line  117 -             $colour /= 4.0
  7%: Line  121 -             if($colour -gt 70) { $colour -= 1 }
  6%: Line  154 -             $nextScreen[$row, $column] = `
  6%: Line  113 -             $colour = $screenBuffer[$baseOffset]
  6%: Line  115 -             $colour += $screenBuffer[$baseOffset + 1]
  6%: Line  116 -             $colour += $screenBuffer[$baseOffset +
  5%: Line  114 -             $colour += $screenBuffer[$baseOffset - 1]
  5%: Line  109 -             $baseOffset = ($windowWidth * $row) +
  0%: Line   90 -         if($random.NextDouble() -ge 0.20)
  0%: Line  152 -         for($column = 0; $column -lt $windowWidth;
  0%: Line   94 -             $screenBuffer[($windowHeight - 2) *

Breakdown by marked regions:
----------------------------
  6%: updateScreen
  0%: startFireLastRow
 93%: propigate
  0%: Unmarked
  0%: main

It looks like we’re spending 93% of our time propagating the fire.  When we investigate how we’re spending the time, none of the hot spots are individually egregious.  Since we’re in such a tight loop, we’re spending most of the time comparing colours.  We can be a little smarter with our ‘if’ statements, attempting to minimize the number of comparisons and variable assignments.  Before optimization, we have 4 checks per loop, and (a weighted average of) just over 1 assignment per loop.  If we write it as below, we reduce that to (a weighted average of) 1 check per loop, and (a weighted average even closer to) 1 assignment per loop.

if($colour -gt 70)

    $colour -= 1 
}
else
{
    $colour -= 3
    
    if($colour -lt 1)
    { 
        $colour = 0 
    }
    elseif($colour -lt 20)
    { 
        $colour -= 1 
    }
}

This brings us up to 0.57 frames per second.  A great improvement, but we’re obviously not done yet.  We run the profiler again, and see that the hotspots have moved:

Breakdown by line:
----------------------------
 13%: Line  141 -             $tempWorkingBuffer[$baseOffset -
  9%: Line  113 -             $colour = $screenBuffer[$baseOffset]
  9%: Line  117 -             $colour /= 4.0
  9%: Line  115 -             $colour += $screenBuffer[$baseOffset + 1]
  9%: Line  131 -                     $colour = 0
  8%: Line  129 -                 if($colour -lt 1)
  8%: Line  127 -                 $colour -= 3
  7%: Line  121 -             if($colour -gt 70)
  7%: Line  116 -             $colour += $screenBuffer[$baseOffset +
  7%: Line  114 -             $colour += $screenBuffer[$baseOffset - 1]
  5%: Line  109 -             $baseOffset = ($windowWidth * $row) +
  5%: Line  167 -             $nextScreen[$row, $column] = `
  3%: Line  244 -             $bufferCell = `
  1%: Line  240 -             $character =
  0%: Line  252 -             $paletteIndex++
  0%: Line  241 -             $fgColour =
  0%: Line  242 -             $bgColour =
  0%: Line  165 -         for($column = 0; $column -lt $windowWidth;

Breakdown by marked region:
----------------------------
  5%: updateScreen
  0%: startFireLastRow
 90%: propigate
  0%: Unmarked
  5%: main

There’s not too much we can do, but there are a few lines representing access and manipulation of the $colour variable as we compute the average.  So we’ll put those into one line:

$colour = ($screenBuffer[$baseOffset] +
    $screenBuffer[$baseOffset - 1] +
    $screenBuffer[$baseOffset + 1] +
    $screenBuffer[$baseOffset + $windowWidth]) / 4.0

The frame rate barely changes, so let’s see what the new hot spots are:

Breakdown by line:
----------------------------
 19%: Line  140 -             $tempWorkingBuffer[$baseOffset -
 13%: Line  113 -             $colour = ($screenBuffer[$baseOffset] +
 12%: Line  109 -             $baseOffset = ($windowWidth * $row) +
 11%: Line  126 -                 $colour -= 3
 11%: Line  128 -                 if($colour -lt 1)
 11%: Line  120 -             if($colour -gt 70)
 10%: Line  130 -                     $colour = 0
  9%: Line  166 -             $nextScreen[$row, $column] = `
  1%: Line  240 -             $fgColour =
  1%: Line  243 -             $bufferCell = `
  0%: Line  239 -             $character =
  0%: Line  122 -                 $colour -= 1
  0%: Line  251 -             $paletteIndex++
  0%: Line  106 -         for($column = 1; $column -lt ($windowWidth - 1);
  0%: Line  219 -                 if($apparentBrightness -gt

Breakdown by marked region:
----------------------------
  9%: updateScreen
  0%: startFireLastRow
 87%: propigate
  0%: Unmarked
  4%: main

There’s really not much left for us to optimize.  We seem to be spending a lot of time computing the $baseOffset variable, though.  Although we can’t improve the efficiency of the statement, we can execute it less frequently.   Rather than compute it from scratch each time via multiplication, we’ll simply increment it once per column.

We can continue with this iterative refinement, giving an end result of about 0.68 frames per second – 70% faster than when we started.  You can download the optimized version here: [burn-console-2.working.msh].  The problems have now shifted to things we have no control over:

Breakdown by line:
----------------------------
 20%: Line  171 -             $nextScreen[$row, $column] = `
 20%: Line  111 -             $colour = ($screenBuffer[$baseOffset] +
 18%: Line  141 -             $tempWorkingBuffer[$baseOffset -
 17%: Line  144 -             $baseOffset++
 17%: Line  118 -             if($colour -gt 0)
  1%: Line  120 -                 if($colour -gt 70)
  1%: Line  248 -             $bufferCell = `
  1%: Line  126 -                     $colour -= 3
  1%: Line  122 -                     $colour -= 1
  0%: Line  134 -                         $colour -= 1
  0%: Line  128 -                     if($colour -lt 1)
  0%: Line  246 -             $bgColour =
  0%: Line   94 -             $screenBuffer[($windowHeight - 2) *
  0%: Line  130 -                         $colour = 0
  0%: Line  162 -     $nextScreen =

Breakdown by marked region:
----------------------------
 20%: updateScreen
  1%: startFireLastRow
 77%: propigate
  0%: Unmarked
  3%: main

To solve this problem, we’ll delve into a trick from the Demo Scene.  Did you ever use inline assembler in C?  We’ll do something similar, but with inline C# in MSH.

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

burn-console Digression: An MSH Script Profiler Part 2

In the last article, we talked about the theory behind a sampling profiler.  Let’s go over the code to implement one.  We’ll use this to guide our future optimizations.  You can also download the script here: profile-transcript.msh.

################################################################################
##
## profile-transcript.msh
##
## Computes the performance characteristics of a script, based on the transcript
## of it running at trace level 1.
##
## To profile a script:
##    1) Turn on MSH script tracing in the window that will run the script: 
##       set-mshdebug –trace 1
##    2) Turn on the transcript for the window that will run the script:
##       start-transcript
##       (Note the filename that Monad provides as the logging destination.)
##    3) Type in the script name, but don't actually start it.
##    4) Open another MSH window, and navigate to the directory holding 
##       this script.  Type in '.\profile-transcript.msh <path-to-transcript>',
##       replacing <path-to-transcript> with the path given in step 2.  Don't
##       press <Enter> yet.
##    5) Switch to the profiled script window, and start the script.
##       Switch to the window containing profile-transcript, and press <Enter>
##    6) Wait until your profiled script exits, or has run long enough to be
##       representative of its work.  To be statistically accurate, your script
##       should run for at least ten seconds.
##    7) Switch to the window running profile-transcript.msh, and press a key.
##    8) Switch to the window holding your profiled script, and type:
##       stop-transcript
##    9) Delete the transcript.
##
## Note: You can profile regions of code (ie: functions) rather than just lines
## by placing the following call at the start of the region:
##       write-debug "ENTER <region_name>"
## and the following call and the end of the region:
##       write-debug "EXIT"
## This is implemented to account exclusively for the time spent in that 
## region, and does not include time spent in regions contained within the
## region.  For example, if FunctionA calls FunctionB, and you've surrounded
## each by region markers, the statistics for FunctionA will not include the
## statistics for FunctionB.
##
################################################################################

param($logFilePath = $(throw "Please specify a path to the transcript log file."))

function Main
{
    ## Run the actual profiling of the script.  $uniqueLines gets
    ## the mapping of line number to actual script content.
    ## $samples gets a hashtable mapping line number to the number of times
    ## we observed the script running that line.
    $uniqueLines = @{}
    $samples = GetSamples $uniqueLines
    
    "Breakdown by line:"
    "----------------------------"

    ## Create a new hash table that flips the $samples hashtable -- 
    ## one that maps the number of times sampled to the line sampled.
    ## Also, figure out how many samples we got altogether.
    $counts = @{}
    $totalSamples = 0;
    foreach($item in $samples.Keys) 
    { 
       $counts[$samples[$item]] = $item 
       $totalSamples += $samples[$item]
    }

    ## Go through the flipped hashtable, in descending order of number of 
    ## sample
s.  As we do so, output the number of samples as a percentage of

    ## the total samples.  This gives us the percentage of the time our script
    ## spent executing that line.
    foreach($count in ($counts.Keys | sort -desc))
    {
       $line = $counts[$count]
       $percentage = "{0:#0}" -f ($count * 100 / $totalSamples)
       "{0,3}%: Line {1,4} -{2}" -f $percentage,$line,
          $uniqueLines[$line]
    }

    ## Go through the transcript log to figure out which lines are part of any
    ## marked regions.  This returns a hastable that maps region names to
    ## the lines they contain.
    ""
    "Breakdown by marked regions:"
    "----------------------------"
    $functionMembers = GenerateFunctionMembers
    
    ## For each region name, cycle through the lines in the region.  As we
    ## cycle through the lines, sum up the time spent on those lines and output
    ## the total.
    foreach($key in $functionMembers.Keys)
    {
        $totalTime = 0
        foreach($line in $functionMembers[$key])
        {
            $totalTime += ($samples[$line] * 100 / $totalSamples)
        }
        
        $percentage = "{0:#0}" -f $totalTime
        "{0,3}%: {1}" -f $percentage,$key
    }
}

## Run the actual profiling of the script.  $uniqueLines gets
## the mapping of line number to actual script content.
## Return a hashtable mapping line number to the number of times
## we observed the script running that line.
function GetSamples($uniqueLines)
{
    ## Open the log file.  We use the .Net file I/O, so that we keep monitoring
    ## just the end of the file.  Otherwise, we would make our timing innacurate
    ## as we scan the entire length of the file every time.
    $logStream = [System.IO.File]::Open($logFilePath, "Open""Read""ReadWrite")
    $logReader = new-object System.IO.StreamReader $logStream

    $random = new-object Random
    $samples = @{}

    $lastCounted = $null
    
    ## Gather statistics until the user presses a key.
    while(-not $host.UI.RawUI.KeyAvailable)
    {
       ## We sleep a slightly random amount of time.  If we sleep a constant
       ## amount of time, we run the very real risk of improperly sampling 
       ## scripts that exhibit periodic behaviour.
       $sleepTime = [int] ($random.NextDouble() * 100.0)
       start-sleep -m $sleepTime

       ## Get any content produced by the transcript since our last poll.
       ## From that poll, extract the last DEBUG statement (which is the last
       ## line executed.)
       $rest = $logReader.ReadToEnd()
       $lastEntryIndex = $rest.LastIndexOf("DEBUG: ")

       ## If we didn't get a new line, then the script is still working on the
       ## last line that we captured.
       if($lastEntryIndex -lt 0
       { 
     &
nbsp;    if($lastCounted) { $samples[$lastCounted] ++ }
          continue
       }
       
       ## Extract the debug line.
       $lastEntryFinish = $rest.IndexOf("\n", $lastEntryIndex)
       if($lastEntryFinish -eq -1) { $lastEntryFinish = $rest.length }

       $scriptLine = $rest.Substring($lastEntryIndex, ($lastEntryFinish - $lastEntryIndex)).Trim()
       if($scriptLine -match 'DEBUG:[ \t]*([0-9]*)\+(.*)')
       {
           ## Pull out the line number from the line
           $last = $matches[1]
           
           $lastCounted = $last
           $samples[$last] ++
           
           ## Pull out the actual script line that matches the line number
           $uniqueLines[$last] = $matches[2]
       }

       ## Discard anything that's buffered during this poll, and start waiting
       ## again
       $logReader.DiscardBufferedData()
    }

    ## Clean up
    $logStream.Close()
    $logReader.Close()
    
    return $samples
}

## Go through the transcript log to figure out which lines are part of any
## marked regions.  This returns a hastable that maps region names to
## the lines they contain.
function GenerateFunctionMembers
{
    ## Create a stack that represents the callstack.  That way, if a marked
    ## region contains another marked region, we attribute the statistics
    ## appropriately.
    $callstack = new-object System.Collections.Stack
    $currentFunction = "Unmarked"
    $callstack.Push($currentFunction)

    $functionMembers = @{}

    ## Go through each line in the transcript file, from the beginning
    foreach($line in (get-content $logFilePath))
    {
        ## Check if we're entering a monitor block
        ## If so, store that we're in that function, and push it onto
        ## the callstack.
        if($line -match 'write-debug "ENTER (.*)"')
        {
            $currentFunction = $matches[1]
            $callstack.Push($currentFunction)
        }
        ## Check if we're exiting a monitor block
        ## If so, clear the "current function" from the callstack,
        ## and store the new "current function" onto the callstack.
        elseif($line -match 'write-debug "EXIT"')
        {
            [void] $callstack.Pop()
            $currentFunction = $callstack.Peek()
        }
        ## Otherwise, this is just a line with some code.
        ## Add the line number as a member of the "current function"
        else
        {
            if($line -match 'DEBUG:[ \t]*([0-9]*)\+')
            {
                ## Create the arraylist if it's not initialized
                if(-not $functionMembers[$currentFunction])
                {
    &
nbsp;               $functionMembers[$currentFunction] = new-object `
                        System.Collections.ArrayList
                }
                
                ## Add the current line to the ArrayList
                if(-not $functionMembers[$currentFunction].Contains($matches[1]))
                {
                    [void] $functionMembers[$currentFunction].Add($matches[1])
                }
            }
        }
    }
    
    return $functionMembers
}

. Main

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

A Legal Take on the JC Penny Price Gouging

AKA: Welcome Don McGowan to the Party 🙂  Don works in the Law and Corporate Affairs organization at Microsoft, and has always been a bright and illuminating voice in our internal discussions.

Case in point -- a bunch of people internally are pretty upset that JC Penny is gouging customers by charging $800 for a vanilla Xbox  Premium system.  Where most people speculate, Don lays down this awesome summary of the situation:

There's plenty of people out there asking why Microsoft doesn't "do something about this".  Well, one reason is that we can't.

It’s great that he’s started blogging.  GrokLaw used to be a good source of information for non-lawyers, but they really rose to fame with their coverage of the SCO case.  Because of that, they’ve really started pandering pretty heavily to the OSDN audience.

[Edit: Fixed a typo]

burn-console digression: an MSH script profiler

We now have implemented a working fire effect in a form that’s extremely easy to read.  The intention of the code is clear, our functions are nice and modular, and the algorithms are clear.  However, we need to deal with some performance issues.  For that, we'll take a small digression.

At this point, if performance is of utmost importance, we might consider simply porting our script to a compiled C# cmdlet.  However, we’ll continue this in MSH, applying highly-targeted optimizations to bring the performance up to an acceptable level.

When tackling performance problems, the first rule is to measure the problem.  I can’t stress this highly enough.  It stuns (and saddens) me when I see people parade around on an optimization binge without ever enlisting the help of a profiler, or other measurement tool.  Unless you can guide your optimization efforts with hard performance data, you are almost certainly directing them to the wrong spots.  Random cute performance hacks will quickly turn your code to garbage, often with no appreciable performance gain.  I’ve done my fair share of micro-optimization, but it’s always been under the guidance of hard data to support it. 

Now, let’s investigate sources of our performance problems by measuring them.

The easiest way is to probably comment out function calls to see their effect on our refresh rate.  If we comment out the calls to both updateBuffer and updateScreen in our Main function, then we get about 8,000 frames per second.  If we comment out only the call to updateBuffer, we get about 3.7 frames per second.  If we comment out only the call to updateScreen, we get about 0.54 frames per second.  We can continue to comment out chunks of code, slowly determining what causes the most drastic effect on our script’s performance.

However, that’s not very elegant.  It’s slow, and inefficient.  We should really rely on a profiler to provide us the information we need.  But MSH doesn’t have a script profiler.

Is that going to stop us?  Not in the least.  In fact, it will barely slow us down.  With a surprisingly small amount of bubble gum and handy twine, we’ll write our own – in MSH, of course.

There are two approaches one can take when writing a profiler.  The first is an instrumenting profiler, and the second is a sampling profiler.

Instrumenting profilers inject code at the beginning and end of every call to measure the time spent in that call.  The idea is fairly straight-forward, but the technique is fairly invasive.  The instrumentation process itself can skew the performance profile of a program, so the profiler needs to calibrate itself to account for that.  In addition, robust code instrumentation is extremely difficult.  Binary instrumentation is much easier, so nearly all instrumenting profilers take this approach.

Sampling profilers semi-randomly peek into a program to see what statement it is currently executing.  If we see the program executing a certain statement half of the times we check, we can be fairly confident that our program is spending about 50% of its time executing that statement.  Likewise, if we see the program executing a certain statement 8 out of the 50 times we check, we can be fairly confident that our program is spending about 16% of its time executing that statement.  When we apply this idea to a much larger sample set, we can fairly accurately map the execution time of each individual statement.

We’re going to primarily implement a sampling profiler for several reasons:

  • There is no concept of an MSH Script Binary.  If we implement an instrumenting profiler, we have only the very perilous path of mangling the script itself.
  • Monad has several bits of infrastructure available that make a sampling profiler surprisingly easy to implement.

So how do we peek in on the MSH engine to see which line of script it’s currently executing?  Very indirectly. 

  1. Turn on MSH script tracing (at level 1,) as outlined as part of Jon Newman’s excellent series of posts.
    MSH:116 C:\Temp >set-mshdebug –trace 1
  2. Turn on file logging for our session:
    MSH:117 C:\Temp >start-transcript
  3. Start our script

Once the script starts, we get entries similar to this in the transcript log for our session:

(…)
DEBUG:  113+             $colour = $screenBuffer[$baseOffset]
DEBUG:  114+             $colour += $screenBuffer[$baseOffset - 1]
DEBUG:  115+             $colour += $screenBuffer[$baseOffset + 1]
DEBUG:  116+             $colour += $screenBuffer[$baseOffset +
$windowWidth]
(…)

Now, we can see what line of script the engine is currently executing by looking at the last bit of output that made it to the log.

Here is an example of the output – we’ll go over the code next time:

Breakdown by line:
----------------------------
 15%: Line  123 -             if($colour -lt 20) { $colour -= 1 }
 14%: Line  122 -             if($colour -le 70) { $colour -= 3 }
 11%: Line  124 -             if($colour -lt 0) { $colour = 0 }
 10%: Line  128 -             $tempWorkingBuffer[$baseOffset -
  8%: Line  117 -             $colour /= 4.0
  7%: Line  121 -             if($colour -gt 70) { $colour -= 1 }
  6%: Line  154 -             $nextScreen[$row, $column] = `
  6%: Line  113 -             $colour = $screenBuffer[$baseOffset]
  6%: Line  115 -             $colour += $screenBuffer[$baseOffset + 1]
  6%: Line  116 -             $colour += $screenBuffer[$baseOffset +
  5%: Line  114 -             $colour += $screenBuffer[$baseOffset - 1]
  5%: Line  109 -             $baseOffset = ($windowWidth * $row) +
  0%: Line   90 -         if($random.NextDouble() -ge 0.20)
  0%: Line  152 -         for($column = 0; $column -lt $windowWidth;
  0%: Line   94 -             $screenBuffer[($windowHeight - 2) *

Breakdown by marked regions:
----------------------------
  6%: updateScreen
  0%: startFireLastRow
 93%: propigate
  0%: Unmarked
  0%: main

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]