Install Android Studio and Emulator

Download Android Studio and copy to your Applications folder. Launch it and complete the install process using the provided wizard. Using the Configure menu at the bottom the welcome screen, open the SDK Manager.

Confirm the SDK location (we will need this later) and that you have the Android 9.0 (Pie) SDK installed.

Close the SDK Manager and open the AVD Manager (from the Configure menu on the Welcome screen).

Create a new Virtual Device accepting the default device.

Select the Pie system image (downloading it if necessary).

On the last page confirm the setup and click Finish.

Once you have a device, click the green triangle to start the emulator.

Once started, the emulator should show the phone starting up.

Install React Native

Install Node.js. It is helpful to have certain packages installed “globally” but it is considered a bad practice to use sudo for this. Instead follow these instructions. Install the remaining tools using the following command:
npm install -g react react-native react-native-cli

Build an App

From a command prompt, enter the following:
react-native init HelloWorld
cd HelloWorld
react-native run-android
This will open a new Terminal with the title “Running Metro Bundler on port 8081.”

Missing JDK

At this point I got an error that I need to install the Java Developer Kit.

Download the JDK.

Open the package and step through the install process. Then try the startup again:
react-native run-android

“SDK location not found”

“SDK location not found. Define location with sdk.dir in the file or with an ANDROID_HOME environment variable.

As we did to handle the global install of Node.js packages, we will add a line to .profile with the SDK, source the file, and try again:
echo “ANDROID_HOME=/Users/jfoster/Library/Android/sdk” >> ~/.profile
source ~/.profile
react-native run-android

This should have solved the problem, but didn’t for me. So, I added a line to the file:
echo “sdk.dir=/Users/jfoster/Library/Android/sdk/” >> ./android/

“adb: command not found”

The next error I got was “adb: command not found”. It turns out that there are some executables that are installed but not found. So we need more updates to our environment variables:
echo “export PATH=$ANDROID_HOME/platform-tools:$PATH” >> ~/.profile
source ~/.profile
react-native run-android
Finally, we get the app running!

Customize the App

Edit App.js to customize the message, save the file, and then in the emulator double tap R to reload the app.

Remote Debugging

Install RNDebugger to support debugging. With the emulator in the foreground, press <Ctrl>+<M> on the keyboard. This should bring up a developer menu. Select “Debug JS Remotely” the Reload (either from the menu or double tap R).

Add some debugging code to App.js; for example, add a line to the end of the file:
console.log(“This is a message from my code to the console”);
Save App.js, then in the emulator double tap R to reload the app. You should see your message in the debugger.

If in Doubt, Restart

Not infrequently things don’t seem to work right and I close the Metro Bundler terminal, quit the app on the phone (tap the share button at the bottom right on the phone then swipe the app up and away), then restart the app (react-native run-android). You could also try quitting the emulator and restart it as well.


Common error resolved below:
– “ERROR: JAVA_HOME is not set”
– “SDK location not found. Define location with sdk.dir in the file or with an ANDROID_HOME environment variable.”


I’m presently teaching a class on mobile application development and have decided to share my notes about how to get things working. For this exercise I’m starting with a clean install of Windows 10 64-bit and I will walk through the steps I took, including the errors I encountered (so that if you get the same errors you might find this blog post!).

Install Android Studio and Emulator

Download Android Studio and install both Android Studio and Android Virtual Device. Check the box to install the Android Virtual Device. Note that the dialog warns that the SDK location should not contain whitespace:


So, I’ll just put the SDK into C:\Android.

The download and unzip process takes some time. When it finishes we have the Welcome screen. At the bottom there is a drop-down menu:

Select the SDK Manager command to confirm that you have installed an SDK in a path without a space. Make a note of this since we will need it for the ANDROID_HOME environment variable later:

Select the AVD (Android Virtual Device) Manager command to view the emulator that got created for you. Note that this is a “Google API” emulator. We may want to create another one with “Google Play”.

Click the green arrow under the Actions column to start an emulator. I got a warning that that the emulator is using a “compatibility renderer”, which is fine with me. The emulator will show in the Task Manager as “qemu-system-x86_64.exe” and will take a lot of memory and CPU!

Install React Native Tools

Install Node.js accepting all the defaults. When that finishes run the following in a command shell:
npm install -g react react-native react-native-cli

Build an App

From a command prompt enter the following:
react-native init HelloWorld
cd HelloWorld
react-native run-android
This will open a new “node” window with the title “Running Metro Bundler on port 8081.”

If you get a Windows Security Alert asking if you want allow an application to communicate on the network, click “Allow access”:

Errors from Missing Environment Variables

On the original command prompt, you may see the following:
“ERROR: JAVA_HOME is not set and no ‘java’ command could be found in your PATH.”
To solve this open your system settings and add an environment variable for
with the value
“C:\Program Files\Android\Android Studio\jre”
Close the node window and reopen the command shell and try again
cd HelloWorld
react-native run-android

The process should get further but is likely to give you an error:
“SDK location not found. Define location with sdk.dir in the file or with an ANDROID_HOME environment variable.”
Just as we did with above with JAVA_HOME, we now need to set an environment variable for
with the SDK location selected above and found on the SDK System Settings:
Close the node window and reopen the command shell and try again
cd HelloWorld
react-native run-android

Customize App

If all goes well this should have a successful build and the Metro window should show a build process and the emulator should show “Welcome to React Native!”.

Open App.js using a text editor:

Replace “Welcome to React Native!” with something personal and save the file.

Return to the emulator and “Double tap R on your keyboard to reload”.

Remote Debugger

Install Chrome to support debugging. With the emulator in the foreground, press <Ctrl>+<M> on the keyboard. This should bring up a developer menu. Select “Debug JS Remotely”:

When Chrome opens, press <Ctrl>+<Shift>+<J> to open the console.

Add some debugging code to App.js; for example, add a line to the end of the file:
console.log(“This is a message from my code to the console”);
Save App.js, then in the emulator double tap R to reload the app. You should see your message in the Chrome console.

Update: See this Docker image.

People sometimes ask about making GemStone available in a Docker container. The demand seems to be driven by a desire for a simplified install/deployment process. In preparation for my ESUG talk on a cloud-hosted GemStone IDE I decided to do some investigation.

Docker is an alternative to a virtual machine (which I’ve written about extensively on this blog) where the guest environment (“container”) shares not just the hardware with the host and other guests, but also the OS kernel. What is isolated is the application and supporting libraries. It does allow simplified deployment and ensures that each installation has exactly the same libraries and other components. The container can also be isolated so that (by default) it does not write outside its boundaries. These are attractive features.


A typical description of Docker suggests that each container uses the host OS, but it really uses the host OS kernel, and on macOS it runs an embedded Linux instance. Thus, a single Docker container can run on various OS hosts. Contrary to my initial misconception, you don’t need a separate Docker container for every OS and version.

A more challenging issue is what should go in the container. Obviously, it should include GemStone (and you would need a different container for each GemStone version), but what else? Should it include a web server? If so, which one? Perhaps not, but it does increase the complexity if you need to install, configure, and coordinate multiple components.

One hurdle is that once built, the software in a container is essentially fixed. You do not upgrade the contained software, you replace the container. Furthermore, I hope your container does not have any persistent data – it’s not supposed to, meaning thou shalt not run a database inside a container. Or, at least, not a production database; running a development database inside a container might be a good idea.

More specifically, you need to make sure that any persistent data is held outside the container, meaning that your container is not so isolated. Volumes are the preferred mechanism for persisting data generated by and used by Docker containers. And you would still need to manage backups and related system administration tasks.

For things like Topaz (a command-line interface to GemStone), you could run a command in the container. For RPC Gems (used by most client applications and IDE tools such as Jade), you should need to have only one port open into the container since the server-side Gem would be inside the container as well.

Overall, I don’t find it very difficult to install GemStone, but if someone were more likely to investigate GemStone if a Docker container were available, then it shouldn’t be too difficult to provide it.

A 5-minute screencast of this blog post can be found here.

Would you like to run GemStone/S 64 Bit on a Microsoft Windows machine? If so, then the Windows Subsystem for Linux makes that possible. Starting with a 64-bit version of Windows 10, follow these steps:

Install Ubuntu 18.04

  1. Run Windows PowerShell as Administrator and enter the following (on one line):
    Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux
  2. Confirm a restart to complete the process.
  3. From the Windows Store application, install Ubuntu 18.04, pin it to the Start menu, and then launch it.
  4. When prompted, enter a new UNIX username and password.

Install GemStone

  1. Update your system and install the unzip utility:
    sudo apt-get update
    sudo apt-get upgrade -y
    sudo apt-get dist-upgrade -y
    sudo apt-get install unzip
  2. In the Linux shell, create some GemStone directories:
    sudo mkdir /opt/gemstone
    sudo chown $USER /opt/gemstone
    cd /opt/gemstone
    mkdir locks log
  3. In a web browser, navigate to the GemStone product page and copy the link for the Linux download. Use wget to download the product (the following is supposed to be one line, but WordPress hides some; copy the whole thing and past into a text editor): wget
  4. Unzip the product:
    unzip GemStone*
  5. Create a symbolic link to the unzipped directory:
    ln -s GemStone* product
  6. Edit your profile to add/update environment variables:
    vim ~/.profile # add the following two lines
    export GEMSTONE=/opt/gemstone/product
    export PATH=$GEMSTONE/bin:$PATH
  7. Update your environment to include the above variables by doing one of the following:
    1. Enter the two lines from step 6 into your command shell;
    2. source ~/.profile ; or,
    3. Close your shell and open a new shell.
  8. Edit the services file to add an entry for NetLDI:
    sudo vim /etc/services # add the following line
    gs64ldi    50377/tcp    # GemStone/S 64 Bit 3.x
  9. Copy in a new database:
    cp $GEMSTONE/bin/extent0.dbf $GEMSTONE/data/
    chmod +w $GEMSTONE/data/extent0.dbf

Start GemStone

Open a new Linux shell to get your environment variables and start GemStone:
startnetldi -g -a $USER

Install Jade into Windows

  1. Open a web browser on the Jade Releases.
  2. Download (Save) (one of the assets) to your Downloads folder.
  3. Open the Downloads folder and extract the download.
  4. Copy the entire Jade folder to C:\Program Files (x86) or any handy directory (it doesn’t need to be anywhere in particular).
  5. Pin Jade.exe to the Start Menu.
  6. Edit C:\Windows\System32\drivers\etc\services (as a Windows administrator using a Windows text editor such as Notepad++) to add an entry for NetLDI:
    gs64ldi    50377/tcp    # GemStone/S 64 Bit 3.4.2

Log in to GemStone with Jade

  1. From the Start Menu, launch Jade. If Windows identifies Jade as an unrecognized app, click More info and then click the Run anyway button.
  2. Ensure that the version shown in Jade is a close match to the GemStone version, and click the Login button.
  3. Now you can explore your GemStone/S database that is running in Windows!

Stop GemStone

From a Linux shell you can stop GemStone:
stopstone gs64stone DataCurator swordfish

A question on the GLASS mailing list raised some interesting questions about garbage collection and persistent executable blocks. Some objects, such as instances of SortedCollection, may contain a reference to an executable block and this persistent reference may be difficult to update and may hold references to other objects that otherwise could be removed by the garbage collection process.

Simple vs. Complex Blocks

The first issue raised by the question is the distinction between a “simple block” and a “complex block.” In Smalltalk a block may reference variables outside the block and, if it does so, then the virtual machine needs to create a more complex data structure to capture the referenced objects (making it a “complex” block). If the block does not reference any variables outside its immediate scope (beyond the block arguments and block temporaries), then the virtual machine can typically use a much simpler implementation (making it a “simple” block).

Because of the extra overhead, complex blocks tend to take more room and be slower to execute. Thus, a common performance tuning activity is replacing complex blocks with simple blocks when possible, and this can occur in application code as well as vendor-supplied libraries. For example, in GemStone/S 64 Bit version 2.x, the implementation of SortedCollection>>#’addAll:’ is essentially the following:

aCollection do: [:each | self add: each].

The problem with this implementation is that the block references self, making it a complex block. In version 3.x the implementation changed (influenced in part by Paul Baumann’s work), and the code is essentially the following:

aCollection accompaniedBy: self do: [:me :each | me add: each].

The block in this code is a simple block since it does not reference self (or anything outside the block), and performance improved.

While this is all very interesting (and in some cases quite useful), I think that the underlying garbage collection problem is not really with simple vs. complex blocks, but with something else.

Class References in Blocks

A code block in a method will contain a reference to the method and thus to a class (or metaclass for a class-side method). Once the code block is persistent, there is a hard reference to the class (and its class variables, class instance variables, and pool variables). Changes to the class schema will create a new “version” of the class but the old version will remain and be referenced by the code block. Even if the old class version is removed from the ClassHistory and all instances of the old version are migrated to the new version, until the code block is replaced in the persistent object, there will be a hard reference to the old class version and its space cannot be reclaimed by the garbage collection process.

To see how this works consider the method SortedCollection>>#’_defaultBlock’:

| block |
block := SortedCollection new _defaultBlock. "anExecBlock2"
block _debugSourceForBlock. "'[ :a :b | a <= b ]'"
block method. "aGsNMethod"
block method isMethodForBlock. "true"
block method inClass. "SortedCollection"

Note that this situation exists whether or not the block is simple or complex and whether or not the block comes from an instance-side method or a class-side method. Thus, this is an orthogonal discussion or a unique concern.

Alternatively, note how an equivalent block can be created by evaluating a String, but does not contain a reference to a Class:

| block |
block := '[ :a :b | a <= b ]' evaluate. "anExecBlock"
block _debugSourceForBlock. "'[ :a :b | a <= b ]'"
block method. "aGsNMethod"
block method isMethodForBlock. "true"
block method inClass. "nil"

So, as suggested one could strip the class reference with the following code:

^ aBlock _sourceString evaluate

Alternatively, one could avoid the private method (#’_sourceString’) by making the String explicit:

^ '[ :a :b | a <= b ]' evaluate

While this avoids the private method, it hides the code in a string making it much more difficult for tools to recognize that this is code (and catch compile errors, support senders/implementors, provide for refactorings, etc.). Further, each of these approaches will create a new instance each time it is called, creating unnecessary objects.

As Dale noted in the email chain referenced above, we could modify the compiler so that a block did not know the class in which it was created. This might improved the garbage collection situation, but would make it more difficult to debug code and track a block back to its source. And, of course, you would need to wait till that feature was added to the product.

One way to address these issues is to create a new class with no instance variables (so that the schema would never change) and use class-side methods to return the desired blocks. The method that would otherwise hold the code block would use a level of indirection to return the actual block. If a code block needed to change, you would create a new class-side method in the special class and edit the indirecting method (so that references to the old code block could be found and modified). The original method would remain as long as there were references to it.

At the cost of a level of indirection (and reduced encapsulation), this gives you actual source code that can be recognized by tools, a single instance, and no stray references from the class (since there is only one class version).

[Note: I found this in “Drafts” and decided to publish it even though the original question was a couple years ago.]


A video of my presentation on GemStone/S and SQL is available here. Unfortunately, the audio is not very strong.

GemStone/S has a configuration STN_TRAN_FULL_LOGGING that, if set to TRUE, causes all transactions to be recorded to the redo-log. In this mode a backup plus transaction logs can be used to recover if the extents are lost. If the configuration is set to FALSE, small transactions are still logged but large transactions cause a “checkpoint” in which all data is written to the extents before the transaction completes. In this mode recovery from a crash is possible if all the extents and the most recent transaction log are available (older logs are typically deleted automatically in this mode). The logs themselves are not sufficient to allow recovery from a backup in partial logging mode, so partial logging mode is inappropriate for production (but handy sometimes for development since old logs are typically deleted automatically).

In some applications or situations it would be nice to segregate transactions into those that should be logged for crash recovery and those that can be easily recreated if needed (and avoid the overhead of time and space in writing to the log). For example, it is common to do a bulk-load of new data before it is needed by the application. If the system crashes, we’d prefer to recover the live data quickly and reload the new data later. With Oracle one can specify a table as being NOLOGGING and then do a series of “Direct-Load INSERT” commands that bypass the rollback/replay log. A key limitation with this capability, however, is that it does not support referential integrity. Likewise, with SQL Server one can do a BULK INSERT that will, under some circumstances, have minimal logging. By default, this will leave the constraint on the table marked as not-trusted.

For a relational database, a foreign key is a value like any other (typically an integer or string) and if a JOIN fails to find a match then the matching data is NULL or the row is ignored. In GemStone/S, an object reference is guaranteed to be valid and there is no easy way to handle data that lacks referential integrity. Thus, if an object is visible in the database (e.g., from a bulk load), then any session can, in a transaction, create a reference to it. If the reference is in the transaction (redo) log, then the object must itself be in the transaction log. Therefore, because in GemStone/S referential integrity is required, not optional, the relational approach to avoiding the transaction log is not safe.

The relational solutions described above for reducing transaction log activity allow for a bulk load of data that can, eventually (following a full back, recreation of indexes, and reapplication of any constraints) be equivalent to all other data. This fits an application upgrade (bulk load) use case quite well.

Another possible use case is short-lived data that is to be shared by multiple sessions but will not be saved over the long-term. One example is transient data where a summary is to be kept but the raw data discarded after the analysis. Another example is information about logged-in sessions with in-progress activity (such as Seaside session state) that could be safely lost in a system crash (presumably the users would need to start over anyway). This use-case does not present the same referential integrity concerns and GemStone/S is developing a feature that is intended to address this use-case: a SymbolDictionary in Globals named #’NotTranloggedGlobals’. The idea is that any object referenced from this root would not be recorded in the transaction log and should not be referenced from any other path. At present this is still a work-in-progress and customers are advised to not use it.

Periodically the International Earth Rotation Service determines that Coordinated Universal Time needs to be adjusted to match the mean solar day. This is because the earth doesn’t rotate at a constant speed and in general takes slightly longer than 86,400 seconds. These adjustments have been done through adding (or, in theory, removing) a second every few years. The next leap second insertion is scheduled for June 30th, 2015 at 23:59:60 UTC. How does GemStone/S handle that?

GemStone gets time from the host OS (Unix Time) and is typically described as the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970. This definition is correct so long as we assume that each and every day has exactly 86,400 seconds. If you take into account the 27 leap seconds that will have been added as of June 30, 2015, it is more accurate to say that Unix Time is the number of atomic seconds that have elapsed since 00:00:27 UTC, 1 January 1970. The way the adjustment occurs is that when there is a leap second, the Unix time counter takes two atomic seconds to advance by one Unix second.

The impact of this approach is that a record of when something happened will be correct, but the time between two events could be reported as being one or more seconds less than the actual time between the events. For example, Unix time (and GemStone) will report that there are five seconds between June 30 at 23:59:55 and July 1 at: 00:00:00 when in fact in 2015 there were six seconds between those two times.

Whether this matters is an application-level or domain-level problem. If keeping track of the time between events needs to be accurate (e.g., recording some rapid physics event), then relying on Unix time (or GemStone) will not be sufficient. If all that is needed is a timestamp and it is acceptable to have June 30, 2015 23:59:59 UTC last for 2000 milliseconds, then things should be fine.

A screencast of this blog post is here.

Because Smalltalk was the origin of much of today’s GUI (mouse, overlapping windows, drop-down menus), Smalltalk developers are understandably accustomed to a nice GUI IDE. GemStone/S is an excellent database and Smalltalk execution environment and includes a built-in command-line tool, Topaz, where you can execute Smalltalk code, but has no native GUI. In this blog post we continue a demonstration of on the Macintosh (started here) and show Jade, a GUI-based IDE available on Microsoft Windows.

We launch on the Macintosh, update the version list, download, then install and start a GLASS extent (image) that includes Monticello/Metacello tools. When the database is running we start a Topaz session and install Seaside 3.0 and Magritte 3 with the following script:

"based on"
MCPlatformSupport commitOnAlmostOutOfMemoryDuring: [
  ConfigurationOfMetacello project updateProject.
  ConfigurationOfMetacello loadLatestVersion.
  Gofer project load: 'Seaside30' group: 'Seaside-Adaptors-Swazoo'.
"based on"
Gofer it
  squeaksource: 'MetacelloRepository';
  package: 'ConfigurationOfMagritte3';
MCPlatformSupport commitOnAlmostOutOfMemoryDuring: [
  ConfigurationOfMagritte3 project stableVersion load.
WAGsSwazooAdaptor new start.

When Swazoo is running, we can go to http://localhost:8080 and see Seaside running locally. This demonstrates running Smalltalk code in Topaz, the command-line tool. Next we look at a GUI-based IDE that runs on a Microsoft Windows client platform.

Jade is available as a 14 MB zip download from It includes an executable, client libraries (DLLs) for various GemStone/S versions (ranging from 32-bit version 6.1 to the latest 64-bit version), and related files (including source code). Like most GemStone/S client GUI tools, it is built in another Smalltalk (in this case, Dolphin Smalltalk from Object Arts), but unlike these other tools, you can’t see the client Smalltalk (unless your load Jade source code into your own Dolphin development environment), so we avoid the the two-object-space confusion. Jade is intended to take you directly to GemStone/S, without going through Pharo, Squeak, VA, or VW Smalltalk.

Jade is also designed to work with the no-cost version of GemStone/S (unlike the VA/VW-based GBS tools), and performs well on a slow network (unlike GemTools).

When you unzip the download, you have a folder with various items. Jade.exe is the primary executable (containing the Dolphin VM and the image) and it relies on Microsoft’s C Runtime Library. There is a copy of the executable in Jade.jpg for sites where executables are stripped from zip files during the download process (simply rename the suffix and it will become executable). Contacts.exe is used sometimes in a training class. The bin directory contains the GCI client libraries and a DLL with various images used in the IDE. You can also see a directory containing source code for Jade.

Screen Shot 2013-10-01 at 10.56.18 AM

When you launch Jade, you get a login window that gives you a place to select the GemStone/S version (which GCI library we will use), and other information needed for a login. The Stone box contains two fields, one for the host/IP of the Stone machine (from the perspective of the Gem machine, so localhost is almost always sufficient), and the name of the Stone. In the screencast mentioned above our stone was named gs64stone1. The Gem box contains a number of fields. Most logins will use an RPC Gem (a Linked Gem is available only on 32-bit Windows) with a Guest-authenticated Gem (if your NetLDI was not started in guest mode (-g), then you will need to provide an OS user and password). An RPC Gem will be started by a NetLDI, so we need to identify the Gem machine (in my example the host is vienna and the NetLDI is listening on port 54120) and the command used to start the Gem (except in rare cases the command will be ‘gemnetobject’). You provide a GemStone User ID and Password (by default, ‘DataCurator’ and ‘swordfish’), and if you are going to use any Monticello features it would be good to identify the developer’s name (one word with CamelCase).

Screen Shot 2013-10-01 at 11.01.49 AM

If you get an error on login, we attempt to give as much explanation as possible. Typically, (1) there is no NetLDI on the host/port (see following example), (2) there is no stone with that name, (3) there is a version mismatch, or (4) you have given an unrecognized user ID or password.

Screen Shot 2013-10-01 at 11.13.38 AM

When you have a successful login, you will get a launcher that consists of several tabs. The Transcript serves the traditional (ANSI) Transcript function of showing output sent to the Transcript stream. The second tab shows information about your current session.

Screen Shot 2013-10-01 at 11.19.59 AM

The third tab shows information about the current logged-in sessions, including where the Gem is located, where the client GCI is located, and whether a Gem is holding the oldest commit record. If you have appropriate security, you can send a SigAbort to a session or even terminate it!

Screen Shot 2013-10-01 at 11.21.07 AM

The final tab is a Workspace. In this tab you can execute, print, and inspect Smalltalk code. You can also use the toolbar or menus to abort or commit and open other tools.

Screen Shot 2013-10-01 at 11.17.23 AM


One of the tools is a User Profile Browser that shows the various users defined in the database.


Screen Shot 2013-10-01 at 11.31.42 AM

Next is a Monticello Repository Browser that shows various repositories, packages, and versions.


Screen Shot 2013-10-01 at 11.32.47 AM

The Monticello Browser includes a tool to browse differences between packages.

Screen Shot 2013-10-01 at 11.33.36 AM


Much of your work will be done in a System Browser. This view shows the four SymbolDictionary instances in my SymbolList. UserGlobals is bold, indicating that it is the ‘home’ or default SymbolDictionary, but I have selected Globals and see a list of class categories, a partial list of classes, and in the lower section of the screen is a list of the non-class objects in Globals (note things like AllGroups and AllUsers).

Screen Shot 2013-10-01 at 11.41.42 AM


This screen shot shows us an example of the Packages view (which requires Monticello), and a method with a breakpoint (the red rectangle around a method).

Screen Shot 2013-10-01 at 11.43.54 AM

There are other tools, including a debugger, but I’ll leave that for your exploration (and/or another post/screencast).

Have fun and let me know if you have questions or feature requests.