Showing posts with label windows. Show all posts
Showing posts with label windows. Show all posts

Friday, 9 October 2015

Getting your priorities right

I want to take a look at implementing prioritisation in redis.workflow. Currently, all workflows have the same priority; redis.workflow is all about throughput. If all tasks are going to be completed by some SLA-agreed time then it doesn’t technically matter which order everything is run in, and we can run through workflows in any order, as long as there’s no slack time in task turnover.

However, there contexts in which pure throughput isn’t enough. Sometimes its social: someone arbitrarily wants to see a graph showing a steady completion of recognizable sets of workflows. Sometimes it’s based on operational information: not all workloads are heterogeneous, and might have long-tail task duration distributions; and those long runners will breach your delivery SLA if you start them too late.  Sometimes you have combined workloads: the system might be responsible for both batch throughput calculations and ad-hoc calculations issued directly by someone who’s waiting for an answer, and wants some guarantee that they’re not wedged behind a freight train of batch work. Sometimes it’s simply SLA based: you have 24 hours worth of work, but some of it needs to be delivered by 8am, and some of it by 5pm.

In many ways then a pure throughput system might not meet all requirements, and in each of the scenarios above a decision needs to be made about each item of work: do I need to get this work done first, or that? The system needs to understand work priority.

In detail it’s also worth considering where the priority lies. Currently I’m considering prioritising workflows only relative to each other. That said it’s as easy to implement task-level prioritisation as it it to implement workflow-level prioritisation; doing the former gives me a little more flexibility, and I (or any client) could replicate workflow-level prioritisation by giving every task in a workflow the same priority.

Prioritisation itself can be simple or sophisticated, but will definitely have consequences for the structures used in redis.workflow.

Currently redis.workflow uses a queue of submitted tasks, which is fine for a simple throughput scenario. I could extend it to make practical binary priority decisions by enqueuing high priority tasks at the front of the queue, and low priority tasks at the back, without the need for a new structure.

If finer grained priorities are required then things get a little harder. If the number of levels of priority is small then having a queue per priority might be feasible; in fact, combined with the enqueue-to-front-or-back trick you only need half the number of queues as you have priority levels.

If very fine-grained or arbitrary priorities are needed, then I’d need to start thinking about maintaining relative order in the queue or list. Insertion at a given priority could happen between work of higher and lower priority, which would require iterating through the list to work out where to put it, or just enqueuing, then sorting the list. That’s likely to perform underwhelmingly; a more sophisticated solution would be to add a lookup that identifies priority boundaries by queue position.

Each of these is a reasonable way of managing prioritised work. Clearly enabling the use of arbitrary priorities would allow clients to choose, and even use simpler (say binary or even fall back to unary) prioritisation, but come at a cost in terms of complexity.

The binary high/low priority solution – enqueuing to front or back – is relatively easy to imagine in my existing code, as it’s already placing work at the back. Jumping to arbitrary priorities is harder, but it’s worth considering what would need to be done.

To manage arbitrarily prioritised work, I’d need to sort the work submitted in some way. Redis does have a sorted set, which offers O(log N) lookup and insertion by score. I’d need to be able to remove the highest-scoring task, which I would be able to do with

zremrangebyscore submitted:<type> <minPriority> <maxPriority> limit 0 1

except that there’s no limit out-of-the-box for zremrangebyscore. Instead, I’d have to use zrange to return a sorted list of items (although this sorts from lowest to highest score, meaning I need to decide whether 0 is low or high priority) then use zrem to remove the task.

I implemented this sorted-set-based prioritisation and ran it thorugh the benchmark app, giving each task the same priority. It was noticeably slower, taking about 485 seconds to run through the benchmark (a churn of about 1.8k tasks per second). Task and’ actual workflow completion seemed to keep pace with submission, but handling of the messages announcing workflow completion seems to fall behind, suggesting that it’s not scaling well in this example.

benchmark-withpriority

The benchmark needs to be extended to test workflows with varying numbers of tasks, and tasks with varying priorities.

For this moment I think that’s enough; there’s a wide range of ways of tackling the prioritisation problem, but the implementation of each is fairly straightforward in Redis. The way I’ve implemented it gives it a certain amount of flexibility – a client can configure priority at task or workflow level, but does slow things down. It’s possible the other, simpler methods have a less detrimental effect on performance; as the difference in performance is so significant it might make sense to consider making use of prioritisation configurable, although that’ll be difficult as it has fundamental consequences for the structure of the data in Redis.

Friday, 15 February 2013

Logging and context in distributed systems

Garry Shutler posted about logging at the end of last year; at the same time I was thinking about some structures that my friend Chris Oldwood had put in place to enrich our logging with contextual information. Gary’s intent was to demonstrate that good logging is a vital part of a distributed system; his example mechanism for relating log messages was necessarily simple, and I’d like to expand it.

“Given sufficient detail in your logs,” Garry says, “you no longer have to theorise or piece together how something happened. Your logs will lay it out for you, plain as day.

Anyone with production experience of a distributed system is likely to agree. If you can identify and tie together related log entries over thread and process boundaries then working out what actually happened becomes possible. An important first step is to have process and thread ids in every log message; the alternative is chaos.

Moreover, it’s not just about stack traces; to replicate a problem you often need to understand the path to failure, and that’s where contextual information helps.

Enriching logs with contextual information

Garry’s simplified example has him manually inserting a username into each log entry, allowing him to tag those entries caused by each user’s actions. This is a not so practical in larger-scale system; in such systems we want contextual information to be tagged automatically onto log messages, and we want to be able to arbitrarily extend that contextual information as we see fit. We can do that with a Diagnostic Context, described in Pattern Languages of Program Design Vol. 3.

Diagnostic contexts are essentially maps associated with a logging subsystem. Developers can place whatever they want into the diagnostic context, and the logging subsystem picks it up and tags log messages with it.

At coarsest grain we want a key piece of information carried from one thread to another – like a session id – that allows you to tie related messages together. At finer grain we might want to signal transition through system layers; or the creation of important contextual information like authorisation for a specific user; or the start and end of significant independent workflows.

As crossing boundaries generally implies marshalling this map in some way, especially in service-oriented architectures, then using a single piece of tie information on either side of the boundary is more efficient than simply reconstituting the entire map.

Log4J has both Nested and MappedDiagnosticContexts. Nested contexts are not map-based but instead allow you to push and pop new context strings onto and from a stack. Mapped contexts do not give you this stack-like behaviour – a push of a new value for an already-defined parameter overrides it. In both cases the context can be inherited from a parent thread but you must clone the entire current context and use the clone to create a new context for the new thread.

Log4Net has a GlobalContext which has stack semantics, and allows the attribution of values to named parameters. It similarly makes use of the Dispose pattern and using { } syntax to make management of contexts simpler.

The team I’m currently with has a home-grown Diagnostic context called Causality which bears a deal of similarity to log4net’s GlobalContext.

A simple C# Diagnostic Context

The team I’m currently with has a diagnostic context called Causality. Causality is plumbed into our logging subsystem. If you’re using an off-the-shelf logger that doesn’t support diagnostic contexts you can just decorate your logger invocations with methods that append information from Causality.

Causality is essentially a thread static map. For convenience it's structured as a set of transactions so that attributes can be updated and reverted rather than just overwritten. A single Transaction represents a change to a single Attribute-Value pair, and the Dispose pattern is hijacked to provide some syntactic sugar:

private sealed class Transaction
{
    public Transaction(Attribute attribute, Value @value)
    {
        // instantiate _attributes map if needed and add key-value pair ...
        // if this is an update of an existing Attribute then store the old
    }

    public Dispose()
    {
        // rollback the attribute if it was modified ...
        // or just remove it if it was new
    }

    [ThreadStatic]
    private readonly static Dictionary<string, string> _attributes;
}

As _attributes is private readonly ThreadStatic it’s safe to use without locking in concurrent operations.

A CompoundTransaction extends this, representing a stack of Transactions that is unwound on Dispose. The CompoundTransaction just provides a simple way of updating the context with a set of Attribute-Value pairs.

Labelling the context -- or appending/updating an Attribute -- is then the creation of a Transaction:

public static IDisposable Label(Attribute attribute, Value @value)
{
    // instantiate a transaction ...

    return transaction;
}

which would be used within a using block:

Log.Info("I'm about to do something ...");
using(Causality.Label(Category.System, "User", "timbarrass")
{
    Log.Info("I'm doing something!"); 
}

Log.Info("I've done something.");

Causality also provides a Format or ToString method that flattens the map to a string representation. Assuming that Log.Info or the logging subsystem uses this method, the result of the code above might look like:

2013-01-22 07:34 INFO 3432 1 I'm about to do something ...
2013-01-22 07:34 INFO 3432 1 I'm doing something! [User=timbarrass]
2013-01-22 07:34 INFO 3432 1 I've done something.

The Transaction holds attributes at thread static scope, which means that the attributes won’t be available when you cross a thread or other boundary. On the other side of the boundary you need to reconstitute Causality with a key piece of information that can tie the two threads together. On the first thread:

Causality.IdentifyPrimaryCausality("Request");

using(Causality.Label("User", "timbarrass"))
using(Causality.Label("Request", "31415))
{
    Attributes causality = Causality.AcquirePrimaryCausality();

    Log.Info("Creating worker thread.");

    var worker = new Worker();
    ThreadPool.Start(worker, causality);

    Log.Info("Worker started.");
}

and on the other side of the thread boundary:

public class Worker
{
    public void Start(object state)
    {
        Causality.InjectPrimaryCausality(state as Attributes);

        using(Causality.Label("Component", "Worker")
        {
            Log.Info("Starting work.");
        }
    }
}

The logs here might look like:

2013-01-22 07:34 INFO 3432 1 Creating worker thread. [User=timbarrass;Request=31415]
2013-01-22 07:34 INFO 3432 2 Starting work. [Request=31415]
2013-01-22 07:34 INFO 3432 1 Worker started, [User=timbarrass;Request=31415]

Friday, 13 April 2012

C# performance counters impact performance? No, but it seems that Chart.SaveImage does.

A little thought later and it looks like no, they don’t impact performance. What I was seeing was the effect of prepping and drawing a chart – on about the same timescale that I was querying for information.

Diagnosing the problem was straightforward; I crosschecked the processor usage for the tool with perfmon. After configuring it to avoid saving down a chart the processor usage dropped to zero.

It is a good example of misleading yourself while looking at performance stats. I could see usage spiking on a period that looked suspicious. I used the VS 2010 in-built profiler and saw that PerformanceCounter.NextValue() was by far the biggest contributor to CPU time, and put two and two together to make five.

I ran the app for a few minutes, rather than the one minute I originally tested with, and the stats inverted. Chart.SaveImage() was overwhelmingly responsible for CPU consumption, with the calling method, Agent.Graph(), taking up ~71% of samples.

Schoolboy misinterpretation, but as always a bit of thought and investigation soon shows up the problem.

Now. Why is Chart.SaveImage() so expensive, and is there a cheaper alternative?

Thursday, 12 April 2012

C# performance counters impact performance?

I’ve been using System.Diagnostics.PerformanceCounter to get at CPU and memory usage stats – but it looks like whenever I call NextValue it spikes CPU usage. Running my app through the VS profiler points straight at NextValue as the main CPU load.

It looks, then, like querying PerformanceCounter.Next value seriously impacts CPU usage – on my dual core laptop it flipflops between 5 and 10% utilization.

I’m pretty sure that the same thing doesn’t happen when you use ProcessExplorer, or TaskManager, or Perfmon. I’ve been trying to work out a native way of getting at the same data, but it’s been surprisingly difficult. I just found these that look like decent leads:

SO question on logging performance metrics

Logman -- command line perfmon use

Writing performance data to a log file

Wednesday, 11 April 2012

Creating a chart programmatically in C# using DataVisualization.Charting

Means you have to reference System.Windows.Forms and System.Windows.Forms.DataVisualization, even though you might be doing this in a console application. That said, it also includes a set of data manipulation functions, some of which are even finance-specific:

using System;
using System.Drawing;
using System.Windows.Forms.DataVisualization.Charting;

namespace Sandbox
{
    class Program
    {
        static void Main(string[] args)
        {
            // set up some data
            var xvals = new[]
                {
                    new DateTime(2012, 4, 4), 
                    new DateTime(2012, 4, 5), 
                    new DateTime(2012, 4, 6), 
                    new DateTime(2012, 4, 7)
                };
            var yvals = new[] { 1,3,7,12 };

            // create the chart
            var chart = new Chart();
            chart.Size = new Size(600, 250);

            var chartArea = new ChartArea();
            chartArea.AxisX.LabelStyle.Format = "dd/MMM\nhh:mm";
            chartArea.AxisX.MajorGrid.LineColor = Color.LightGray;
            chartArea.AxisY.MajorGrid.LineColor = Color.LightGray;
            chartArea.AxisX.LabelStyle.Font = new Font("Consolas", 8);
            chartArea.AxisY.LabelStyle.Font = new Font("Consolas", 8);
            chart.ChartAreas.Add(chartArea);

            var series = new Series();
            series.Name = "Series1";
            series.ChartType = SeriesChartType.FastLine;
            series.XValueType = ChartValueType.DateTime;
            chart.Series.Add(series);

            // bind the datapoints
            chart.Series["Series1"].Points.DataBindXY(xvals, yvals);

            // copy the series and manipulate the copy
            chart.DataManipulator.CopySeriesValues("Series1", "Series2");
            chart.DataManipulator.FinancialFormula(
                FinancialFormula.WeightedMovingAverage, 
                "Series2"
            );
            chart.Series["Series2"].ChartType = SeriesChartType.FastLine;

            // draw!
            chart.Invalidate();

            // write out a file
            chart.SaveImage("chart.png", ChartImageFormat.Png);
        }
    }
}

Gives the following result:

chart

Wednesday, 14 March 2012

RRDTool, and C# .NET bindings

RRDTool is pretty much ubiquitous in Linux/Unix cluster environments – or at least, it is in those I’ve worked with. It provides simple but highly reliable round-robin database services into which you can dump pretty much any metric you like. It’ll keep a running dataset at the granularity and duration you define; enable you to export data in chart and xml form; and even perform some useful analysis.

These days I’m typically working in Windows environment, and typically most clients I work for need some sort of dashboard to aggregate information about their clusters and grids. For a long time RRDTool was a pain to build on Windows, and there was no single .NET wrapper. The front runner – NHawk – actually spawned rrdtool processes rather than wrapping the underlying libs direct.

Now it turns out that someone’s contributed .NET bindings to the trunk of RRDTool. I figured I’d see if I could get it working, on 64bit Win7 (although actually x86 is fine for my app), under Visual Studio 2010.

I checked trunk out from SVN and took a look.

The first thing is to set up the dependencies – I tried to take a short cut and begin with a GTK+ package, but found it’s best to start with the files mentioned explicitly in the WIN32_BUILD_TIPS file.

There were a few apparently non-serious build errors in the rrdlib project. In several places explicit casts from void* were required on malloc, realloc. Also, there was some skipping of variable initialization in switch statements – latter can be fixed just by enclosing in a block.

Running through the tutorial with rrdtutorial raised one problem – an error claiming “No positional legend found”. The error was generated in rrd_graph_helper.c. The log showed that this was a work in progress, so I updated to the preceding version, built and it worked apparently fine.

After that I built the .NET bindings – basically fine, although I wrangled everything to x86 in the end just to get started. But then – nothing was being exported. There’s a .def file in the lib solution, but it doesn’t appear to exist any more. So: I wrapped a copy of the public method declarations in #ifdef WIN32, and prefixed with __declspec(dllexport), then rebuilt the .NET binding solution and: success.

All looks good to go. Next step is to grab metrics from windows performance counters in a C# app and see what I can present through RRDTool.

Wednesday, 17 August 2011

System.BadImageFormatException with NUnit on 64bit Windows

This wasted a couple of hours until inspiration struck. In this case it was very useful to have a standalone app that ran the code through its paces without running unit tests.

I’d been developing tests for an x86-targeted library. The tests were fine on our 32bit workstations, but broke when we ran on our 64 bit build machine.

System.BadImageFormatException : Could not load file or assembly ‘XXX, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. An attempt was made to load a program with an incorrect format. 

Tests run on dev workstations went fine.

peverify for the assembly indicated that it was fine.

A simple app that just called LoadLibrary for the assembly indicated it was fine.

A sophisticated proprietary test app that ran the code through its paces behaved as expected.

Build machine was 64 bit OS, compared to 32 bit workstations. I assumed – because of the above – that the library was fine, so I couldn’t figure out why the test was failing. I couldn’t hit the right search on Google for it until it struck me that of course – it was just the nunit tests that were failing. Searching for:

nunit badimageformatexception 64 bit

immediately brought up a wealth of information. On 64 bit machines you need to run the specific x86-targeted nunit apps to test x86-targeted assemblies.

http://stackoverflow.com/questions/208985/nunit-exe-cannot-work-on-vista-64bits-if-x86-build

Thursday, 28 July 2011

Azure: pretty expensive for web hosting

I can almost hear the response now: No! Really? Anyhow …

I thought I’d take a look at prices for hosting a small website on random vanilla web hosts, and for the same on Azure and Amazon.

Random web hosts appear to sit around £2.49 a month for a small website, at least to start with.

An Azure Extra Small instance costs $0.05 per deployed hour – or a whopping $36 a month. You get an order of magnitude more storage at 20GB, but still .. I’m unlikely to need it.

Interestingly I could also dump the whole site into Azure storage. At $0.15 per GB per hour, with transaction fees. I don’t know for sure, but am pretty sure it’s not pro-rata-d for usage less than a GB.

For comparison an Amazon Micro instance costs $0.03 an hour, or about $22 a month.

Or, I guess I could host something at home from a dynamic IP, which probably fits somewhere in the middle in terms of cost.

At any rate, at 10 times the cost Azure doesn’t look like a rational option.

Tuesday, 23 November 2010

Conway's Game of Life

Conway’s Game of Life must be one of the most continuously re-implemented algorithms in the history of software development. In case you’ve not seen it, the Game of Life is played out with cellular automata, or cells that are switched on or off by a set of rules. The rule set is applied to the all cells in one step, creating a new state or generation repeatedly.

A cellular automata implementation shares much with other problems often solved with stepwise and parallelizable modelling – as say some approaches to quantum electrodynamics, fluid mechanics and other physical models – so I figured it was as good a motif as any to explore some new technologies, and maybe work up some kata along the way.

The first step was to build up a core library code that implements the Game of Life. For a first implementation we need a grid of cells on which to play out the game, and a rule set that transforms the grid from one state to another.

This immediately sounded like a good basis for relatively quick kata. There are a couple of useful principles involved: practically, it’s immediately amenable to unit testing as we explicitly define the state we expect from any transformation; and the outline of the algorithm – applying a rule set to a whole state to transform it into a new state – is a useful one to remember.

So, to outline the kata, we need:

  1. A 2d grid that reports whether a cell at x,y is on (alive); allows us to set on/off state at x,y; allows us to clear (all off) and reset (random on/off); and perhaps display a simple representation of the grid to stdout.
  2. A rule set that transforms a 2d grid and returns a new 2d grid with some rule set applied.
  3. Unit tests should express the rules in terms of expected start and end grid states.

The rules – well they’re written up in many places, but to summarise:

  1. Cells with fewer than 2 neighbours die.
  2. Cells with 2 neighbours that are alive, stay alive.
  3. Cells with 3 neighbours stay alive if alive, or come to life if dead.
  4. Cells with 4 or more neighbours die.

The last time I went through this kata my set of C# NUnit tests looked like this – but I’d say the idea is to start from scratch each time:

    [TestFixture]
    public class CoreTests
    {
        [Test]
        [ExpectedException("System.ArgumentOutOfRangeException")]
        public void StubGridDoesntTakeNegativeWidth()
        {
            var g = new StubLifeGrid(-1, 1);
        }

        [Test]
        [ExpectedException("System.ArgumentOutOfRangeException")]
        public void StubGridDoesntTakeNegativeHeight()
        {
            var g = new StubLifeGrid(1, -1);
        }

        [Test]
        [ExpectedException("System.ArgumentOutOfRangeException")]
        public void StubGridOutOfRangeWidthRequestFails()
        {
            var g = new StubLifeGrid(2, 2);
            g.AliveAt(3, 1);
        }

        [Test]
        [ExpectedException("System.ArgumentOutOfRangeException")]
        public void StubGridOutOfRangeHeightRequestFails()
        {
            var g = new StubLifeGrid(2, 2);
            g.AliveAt(1, 3);
        }

        [Test]
        [ExpectedException("System.ArgumentOutOfRangeException")]
        public void StubGridSetOutOfRangeWidthFails()
        {
            var g = new StubLifeGrid(2, 2);
            g.Set(3, 1, true);
        }

        [Test]
        [ExpectedException("System.ArgumentOutOfRangeException")]
        public void StubGridSetOutOfRangeHeightFails()
        {
            var g = new StubLifeGrid(2, 2);
            g.Set(1, 3, true);
        }

        [Test]
        public void DiesWithNoNeighbours()
        {
            ILifeGrid g = new StubLifeGrid(3, 3);
            // Starts alive
            g.Set(1, 1, true);
            
            var r = new BasicRuleSet();
            g = r.Transform(g);

            Assert.IsFalse(g.AliveAt(1, 1));

            // Starts dead
            g = new StubLifeGrid(3, 3);
            g.Set(1, 1, false);

            g = r.Transform(g);

            Assert.IsFalse(g.AliveAt(1, 1));
        }

        [Test]
        public void DiesWithOneNeighbour()
        {
            ILifeGrid g = new StubLifeGrid(3, 3);
            // Starts alive
            g.Set(1, 1, true);
            g.Set(0, 0, true); // create all possible and test

            var r = new BasicRuleSet(); // static, in fact?
            g = r.Transform(g);

            Assert.IsFalse(g.AliveAt(1, 1));

            // Starts dead
            g = new StubLifeGrid(3, 3);
            g.Set(1, 1, false);
            g.Set(0, 0, true);

            g = r.Transform(g);

            Assert.IsFalse(g.AliveAt(1, 1));
        }

        [Test]
        public void StaysAliveWithTwoNeighbours()
        {
            ILifeGrid g = new StubLifeGrid(3, 3);
            g.Set(1, 1, true);
            g.Set(0, 0, true);
            g.Set(0, 1, true);
            g.Display();

            var r = new BasicRuleSet(); // static, in fact?
            g = r.Transform(g);
            g.Display();

            Assert.IsTrue(g.AliveAt(1, 1));

            // starts dead, stays dead
            g = new StubLifeGrid(3, 3);
            g.Set(1, 1, false);
            g.Set(0, 0, true);
            g.Set(0, 1, true);
            g.Display();

            r = new BasicRuleSet(); // static, in fact?
            g = r.Transform(g);
            g.Display();

            Assert.IsFalse(g.AliveAt(1, 1));
        }

        [Test]
        public void BornWithThreeNeighbours()
        {
            ILifeGrid g = new StubLifeGrid(3, 3);
            g.Set(1, 1, false);
            g.Set(0, 0, true);
            g.Set(0, 1, true);
            g.Set(0, 2, true);
            g.Display();

            var r = new BasicRuleSet(); // static, in fact?
            g = r.Transform(g);
            g.Display();

            Assert.IsTrue(g.AliveAt(1, 1));
        }

        [Test]
        public void StaysAliveWithThreeNeighbours()
        {
            ILifeGrid g = new StubLifeGrid(3, 3);
            g.Set(1, 1, true);
            g.Set(0, 0, true);
            g.Set(0, 1, true);
            g.Set(0, 2, true);

            var r = new BasicRuleSet(); // static, in fact?
            g = r.Transform(g);

            Assert.IsTrue(g.AliveAt(1, 1));
        }
    }

Friday, 19 November 2010

jdb on Windows: connecting to the android emulator

I’ve been trying to connect

jdb
to the android emulator for a little while, and have been met repeatedly with:

jdb -sourcepath ./src -attach localhost:8700

java.io.IOException: shmemBase_attach failed: The system cannot find the file specified
        at com.sun.tools.jdi.SharedMemoryTransportService.attach0(Native Method)
        at com.sun.tools.jdi.SharedMemoryTransportService.attach(SharedMemoryTransportService.java:90)
        at com.sun.tools.jdi.GenericAttachingConnector.attach(GenericAttachingConnector.java:98)
        at com.sun.tools.jdi.SharedMemoryAttachingConnector.attach(SharedMemoryAttachingConnector.java:45)
        at com.sun.tools.example.debug.tty.VMConnection.attachTarget(VMConnection.java:358)
        at com.sun.tools.example.debug.tty.VMConnection.open(VMConnection.java:168)
        at com.sun.tools.example.debug.tty.Env.init(Env.java:64)
        at com.sun.tools.example.debug.tty.TTY.main(TTY.java:1010)

Fatal error:
Unable to attach to target VM.

Not so great. So: it appears that jdb on Windows defaults to a shared memory connection with a remote VM. Can we specify a different connector? Turns out we can:

C:\Users\tim\Documents\Android\Life>jdb –sourcepath .\src -connect com.sun.jdi.SocketAttach:hostname=localhost,port=8700

Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
Initializing jdb ...
> 

Beforehand you need to do some setup -- for example, see this set of useful details on setting up a non-eclipse debugger. It includes a good tip for setting your initial breakpoint -- create or edit a

jdb.ini
file in your home directory, with content like:

stop at com.alembic.life.standalone.Life:14

and they'll get loaded and deferred until connection.