Featured

Many Browsers At Once

One Browser Per Workflow

Nothing is shared between workflows, so parallel browsers are simply parallel workflows that each build one. Browser type and automation engine are chosen per browser rather than globally, which is why one workflow can drive Chrome on OttoMagic while the next drives Edge on Puppeteer and a third drives FireFox on Selenium, all in the same process and all at the same time. Give each one a window rectangle if you intend to read them afterwards, because three maximised windows are one window you can see and two you cannot.

Rectangle SlotFor(int row, int rows)

{

Rectangle screen = Screen.PrimaryScreen.WorkingArea;

int gap = 8;

int width = (screen.Width - (gap * (rows + 1))) / rows;


return new Rectangle(screen.Left + gap + (row * (width + gap)),

screen.Top + gap, width, screen.Height * 3 / 4);

}


GPAL.Workflow

.WithWorkflow(() => Drive(BrowserType.Chrome, AutomationEngine.OttoMagic, SlotFor(0, 3)))

.WithWorkflow(() => Drive(BrowserType.Edge, AutomationEngine.PuppeteerPort, SlotFor(1, 3)))

.WithWorkflow(() => Drive(BrowserType.FireFox, AutomationEngine.Selenium, SlotFor(2, 3)))

.WithMaxAtOnce(3)

.Run(out List<WorkflowRun> runs);

NOTE

Worth knowing before you set MaxAtOnce high. An OttoMagic browser learns where its GPALRestAPI is by listening on a fixed local port for the announcement, and the announcement is a bare port number with nothing in it to say which browser sent it. So one launch owns that port at a time, and the next waits for the whole of the one before it: start the browser, load the extension, hear the port back. Measured on a three browser run, that was around seventeen seconds each. The throttle and the stagger do not change this, and neither does raising MaxAtOnce. Three OttoMagic browsers take three times as long to reach the page as one, however many you allow in flight. Selenium and Puppeteer browsers have no such handshake and do start together, so a mixed run is only as serial as its OttoMagic rows.

Profiles Are the Collision

A browser profile belongs to one browser at a time, and this is the single most common reason a parallel run fails. Note what the collision is actually about: it is the profile, not the browser type. Two Chrome browsers run side by side quite happily so long as they are on different profiles, and two on temporary profiles never collide at all. If a browser is already open on the profile you named, Chrome and Edge hand the new launch straight to the running instance and the new process exits, so there is no remote debugging port to connect to and no OttoMagic extension in the process you thought you started. FireFox fails differently and more visibly: geckodriver cannot take a profile that is still locked, so the browser starts and closes immediately and the driver reports that the process closed unexpectedly. Watch for this when a program keeps one profile per browser type, because then two workflows naming the same type are naming the same profile without saying so.

// each browser gets a profile of its own, one per type

static readonly Dictionary<BrowserType, string> profiles = new Dictionary<BrowserType, string>

{

[BrowserType.Chrome] = @"C:ProfilesChromeAutomation",

[BrowserType.Edge] = @"C:ProfilesEdgeAutomation",

[BrowserType.FireFox] = @"C:ProfilesFirefoxAutomation",

};


IBrowser browser = GPAL.Browser

.WithBrowserType(browserType)

.WithAutomationEngine(engine)

.ToGPALObject();


browser.WithProfileDataDirectory(profiles[browserType]);

WARNING

A supplied profile is what makes a run resemble a person rather than a fresh install, and it is worth having. It is also the thing that cannot be shared. Before a parallel run, close any browser already open on the profiles you are about to name, and check for orphans left behind by an earlier run that was killed rather than closed. When you do not need a real profile, name none: each browser then starts on a temporary profile of its own, which cannot collide with anything.

Ports Sort Themselves Out

Ports are not something you have to allocate. Puppeteer browsers take a free debugging port found for them at launch, and GPALRestAPI picks its own port for OttoMagic, trying 3000 first and incrementing until it finds a free one, then announces it while starting. GPAL reads that announcement, so browser.RestApiBaseUrl tells you where a given browser ended up. What this means for parallel work is that two OttoMagic browsers in one process are on two different ports and neither had to be told, and that the port in a log line identifies which browser produced it.

GPAL.Workflow

.WithWorkflow(() =>

{

IBrowser one = GPAL.Browser

.WithBrowserType(BrowserType.Chrome)

.WithAutomationEngine(AutomationEngine.OttoMagic)

.GoTo("https://example.com")

.ToGPALObject();


// where this browser ended up, not where it was told to go

GPAL.PublishSimpleEvent(GPALEventType.NOTICE, $"This browser answers on [{one.RestApiBaseUrl}]");

one.Close(true);

})

.WithMaxAtOnce(2)

.WithStaggerStart(250)

.Run();

NOTE

The default quarter second between starts is there because finding a free port and starting a process is what a browser does first. A dozen browsers doing that in the same instant are a dozen asking the machine the same question before any of them has answered it.

💬 Ask GPAL