Skip to content

Commit b35cb37

Browse files
1 parent a176304 commit b35cb37

File tree

6 files changed

+118
-41
lines changed

6 files changed

+118
-41
lines changed
+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
module Plotly.NET.ImageExport.AsyncHelper
2+
3+
open System.Threading
4+
open System.Threading.Tasks
5+
6+
(*
7+
8+
This is a workaround to avoid deadlocks
9+
10+
https://medium.com/rubrikkgroup/understanding-async-avoiding-deadlocks-e41f8f2c6f5d
11+
12+
TL;DR in many cases, for example, GUI apps, SynchronizationContext
13+
is overriden to *post* the executing code on the initial (UI) thread. For example,
14+
consider this code
15+
16+
public async Task OnClick1()
17+
{
18+
var chart = ...;
19+
var base64 = ImageExport.toBase64PNGStringAsync()(chart).Result;
20+
myButton.Text = base64;
21+
}
22+
23+
Here we have an async method. Normally you should use await and not use .Result, but
24+
assume for some reason the sync version is used. What happens under the hood is,
25+
26+
public async Task OnClick1()
27+
{
28+
var chart = ...;
29+
var task = ImageExport.toBase64PNGStringAsync()(chart);
30+
task.ContinueWith(() =>
31+
UIThread.Schedule(() =>
32+
myButton.Text = Result;
33+
)
34+
);
35+
task.Wait();
36+
}
37+
38+
(this is pseudo-code)
39+
40+
So basically, we set the task to wait until it finishes. However, part of it being
41+
finished is to actually execute the code with button.Text = .... The waiting happens
42+
on the UI thread, exactly on the same thread as where we're waiting for it to do
43+
another job!
44+
45+
That's not the only place we potentially deadlock by using fake synchronous functions.
46+
The reason why it happens, is because frameworks (or actually anyone) override
47+
SynchronizationContext. In GUI and game development it's very useful to keep UI logic
48+
on one thread. But our rendering does not ever callback to it, we're independent of
49+
where the logic actually happens.
50+
51+
That's why what we do is we set the synchronization context to null, do the job, and
52+
then restore it. It is a workaround, because it doesn't have to work everywhere and
53+
independently. But it will work for most cases.
54+
55+
When will it also break? For example, if we decide to take in some callback as a para-
56+
meter, which potentially accesses the UI thread (or whatever). In Unity, for instance,
57+
you can only access Unity API from the main thread. So our fake synchronous function
58+
will crash in the end, because due to the overriden (by us) sync context, the callback
59+
will be executed in some random thread (as opposed to being posted back to the UI one).
60+
61+
However, our solution should work in most cases.
62+
63+
Credit to [@DaZombieKiller](https://github.com/DaZombieKiller) for helping.
64+
65+
*)
66+
67+
let runSync job input =
68+
let current = SynchronizationContext.Current
69+
SynchronizationContext.SetSynchronizationContext null
70+
try
71+
job input
72+
finally
73+
SynchronizationContext.SetSynchronizationContext current
74+
75+
let taskSync (task : Task<'a>) = task |> runSync (fun t -> t.Result)
76+
77+
let taskSyncUnit (task : Task) = task |> runSync (fun t -> t.Wait())

src/Plotly.NET.ImageExport/ChartExtensions.fs

+6-6
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ module ChartExtensions =
5656
fun (gChart: GenericChart) ->
5757
gChart
5858
|> Chart.toBase64JPGStringAsync (?EngineType = EngineType, ?Width = Width, ?Height = Height)
59-
|> Async.RunSynchronously
59+
|> AsyncHelper.taskSync
6060

6161
/// <summary>
6262
/// Returns an async function that saves a GenericChart as JPG image
@@ -97,7 +97,7 @@ module ChartExtensions =
9797
fun (gChart: GenericChart) ->
9898
gChart
9999
|> Chart.saveJPGAsync (path, ?EngineType = EngineType, ?Width = Width, ?Height = Height)
100-
|> Async.RunSynchronously
100+
|> AsyncHelper.taskSync
101101

102102
/// <summary>
103103
/// Returns an async function that converts a GenericChart to a base64 encoded PNG string
@@ -134,7 +134,7 @@ module ChartExtensions =
134134
fun (gChart: GenericChart) ->
135135
gChart
136136
|> Chart.toBase64PNGStringAsync (?EngineType = EngineType, ?Width = Width, ?Height = Height)
137-
|> Async.RunSynchronously
137+
|> AsyncHelper.taskSync
138138

139139
/// <summary>
140140
/// Returns an async function that saves a GenericChart as PNG image
@@ -175,7 +175,7 @@ module ChartExtensions =
175175
fun (gChart: GenericChart) ->
176176
gChart
177177
|> Chart.savePNGAsync (path, ?EngineType = EngineType, ?Width = Width, ?Height = Height)
178-
|> Async.RunSynchronously
178+
|> AsyncHelper.taskSync
179179

180180
/// <summary>
181181
/// Returns an async function that converts a GenericChart to a SVG string
@@ -211,7 +211,7 @@ module ChartExtensions =
211211
fun (gChart: GenericChart) ->
212212
gChart
213213
|> Chart.toSVGStringAsync (?EngineType = EngineType, ?Width = Width, ?Height = Height)
214-
|> Async.RunSynchronously
214+
|> AsyncHelper.taskSync
215215

216216
/// <summary>
217217
/// Returns an async function that saves a GenericChart as SVG image
@@ -251,4 +251,4 @@ module ChartExtensions =
251251
fun (gChart: GenericChart) ->
252252
gChart
253253
|> Chart.saveSVGAsync (path, ?EngineType = EngineType, ?Width = Width, ?Height = Height)
254-
|> Async.RunSynchronously
254+
|> AsyncHelper.taskSync
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace Plotly.NET.ImageExport
22

3+
open System.Threading.Tasks
34
open Plotly.NET
45

56
/// <summary>
@@ -8,31 +9,31 @@ open Plotly.NET
89
type IGenericChartRenderer =
910

1011
///<summary>Async function that returns a base64 encoded string representing the input chart as JPG file with the given width and height</summary>
11-
abstract member RenderJPGAsync: int * int * GenericChart.GenericChart -> Async<string>
12+
abstract member RenderJPGAsync: int * int * GenericChart.GenericChart -> Task<string>
1213
///<summary>Function that returns a base64 encoded string representing the input chart as JPG file with the given width and height</summary>
1314
abstract member RenderJPG: int * int * GenericChart.GenericChart -> string
1415

1516
///<summary>Async function that saves the input chart as JPG file with the given width and height at the given path</summary>
16-
abstract member SaveJPGAsync: string * int * int * GenericChart.GenericChart -> Async<unit>
17+
abstract member SaveJPGAsync: string * int * int * GenericChart.GenericChart -> Task<unit>
1718
///<summary>Function that saves the input chart as JPG file with the given width and height at the given path</summary>
1819
abstract member SaveJPG: string * int * int * GenericChart.GenericChart -> unit
1920

2021
///<summary>Async function that returns a base64 encoded string representing the input chart as PNG file with the given width and height</summary>
21-
abstract member RenderPNGAsync: int * int * GenericChart.GenericChart -> Async<string>
22+
abstract member RenderPNGAsync: int * int * GenericChart.GenericChart -> Task<string>
2223
///<summary>Function that returns a base64 encoded string representing the input chart as PNG file with the given width and height</summary>
2324
abstract member RenderPNG: int * int * GenericChart.GenericChart -> string
2425

2526
///<summary>Async function that saves the input chart as PNG file with the given width and height at the given path</summary>
26-
abstract member SavePNGAsync: string * int * int * GenericChart.GenericChart -> Async<unit>
27+
abstract member SavePNGAsync: string * int * int * GenericChart.GenericChart -> Task<unit>
2728
///<summary>Function that saves the input chart as PNG file with the given width and height at the given path</summary>
2829
abstract member SavePNG: string * int * int * GenericChart.GenericChart -> unit
2930

3031
///<summary>Async function that returns a string representing the input chart as SVG file with the given width and height</summary>
31-
abstract member RenderSVGAsync: int * int * GenericChart.GenericChart -> Async<string>
32+
abstract member RenderSVGAsync: int * int * GenericChart.GenericChart -> Task<string>
3233
///<summary>Function that returns string representing the input chart as SVG file with the given width and height</summary>
3334
abstract member RenderSVG: int * int * GenericChart.GenericChart -> string
3435

3536
///<summary>Async function that saves the input chart as SVG file with the given width and height at the given path</summary>
36-
abstract member SaveSVGAsync: string * int * int * GenericChart.GenericChart -> Async<unit>
37+
abstract member SaveSVGAsync: string * int * int * GenericChart.GenericChart -> Task<unit>
3738
///<summary>Function that saves the input chart as SVG file with the given width and height at the given path</summary>
3839
abstract member SaveSVG: string * int * int * GenericChart.GenericChart -> unit

src/Plotly.NET.ImageExport/Plotly.NET.ImageExport.fsproj

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<ItemGroup>
2626
<None Include="..\..\docs\img\logo.png" Pack="true" PackagePath="\" />
2727
<None Include="RELEASE_NOTES.md" />
28+
<Compile Include="AsyncHelper.fs" />
2829
<Compile Include="IGenericChartRenderer.fs" />
2930
<Compile Include="PuppeteerSharpRenderer.fs" />
3031
<Compile Include="ExportEngine.fs" />

src/Plotly.NET.ImageExport/PuppeteerSharpRenderer.fs

+21-27
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
namespace Plotly.NET.ImageExport
22

3+
open System.Threading
4+
open System.Threading.Tasks
35
open Plotly.NET
46
open PuppeteerSharp
57

@@ -20,7 +22,7 @@ module PuppeteerSharpRendererOptions =
2022

2123

2224
type PuppeteerSharpRenderer() =
23-
25+
2426
/// adapted from the original C# implementation by @ilyalatt : https://github.com/ilyalatt/Plotly.NET.PuppeteerRenderer
2527
///
2628
/// creates a full screen html site for the given chart
@@ -61,7 +63,7 @@ type PuppeteerSharpRenderer() =
6163
///
6264
/// attempts to render a chart as static image of the given format with the given dimensions from the given html string
6365
let tryRenderAsync (browser: Browser) (width: int) (height: int) (format: StyleParam.ImageFormat) (html: string) =
64-
async {
66+
task {
6567
let! page = browser.NewPageAsync() |> Async.AwaitTask
6668

6769
try
@@ -71,41 +73,33 @@ type PuppeteerSharpRenderer() =
7173
return imgStr
7274

7375
finally
74-
page.CloseAsync() |> Async.AwaitTask |> Async.RunSynchronously
76+
page.CloseAsync() |> AsyncHelper.taskSyncUnit
7577
}
7678

77-
/// attempts to render a chart as static image of the given format with the given dimensions from the given html string
78-
let tryRender (browser: Browser) (width: int) (height: int) (format: StyleParam.ImageFormat) (html: string) =
79-
tryRenderAsync browser width height format html |> Async.RunSynchronously
80-
8179
/// Initalizes headless browser
8280
let fetchAndLaunchBrowserAsync () =
83-
async {
81+
task {
8482
match PuppeteerSharpRendererOptions.localBrowserExecutablePath with
8583
| None ->
8684
use browserFetcher = new BrowserFetcher()
8785

88-
let! revision = browserFetcher.DownloadAsync() |> Async.AwaitTask
86+
let! revision = browserFetcher.DownloadAsync()
8987

9088
let launchOptions =
9189
PuppeteerSharpRendererOptions.launchOptions
9290

9391
launchOptions.ExecutablePath <- revision.ExecutablePath
9492

95-
return! Puppeteer.LaunchAsync(launchOptions) |> Async.AwaitTask
93+
return! Puppeteer.LaunchAsync(launchOptions)
9694
| Some p ->
9795
let launchOptions =
9896
PuppeteerSharpRendererOptions.launchOptions
9997

10098
launchOptions.ExecutablePath <- p
10199

102-
return! Puppeteer.LaunchAsync(launchOptions) |> Async.AwaitTask
100+
return! Puppeteer.LaunchAsync(launchOptions)
103101
}
104102

105-
/// Initalizes headless browser
106-
let fetchAndLaunchBrowser () =
107-
fetchAndLaunchBrowserAsync () |> Async.RunSynchronously
108-
109103
/// skips the data type part of the given URI
110104
let skipDataTypeString (base64: string) =
111105
let imgBase64StartIdx =
@@ -120,7 +114,7 @@ type PuppeteerSharpRenderer() =
120114
interface IGenericChartRenderer with
121115

122116
member this.RenderJPGAsync(width: int, height: int, gChart: GenericChart.GenericChart) =
123-
async {
117+
task {
124118
use! browser = fetchAndLaunchBrowserAsync ()
125119

126120
return! tryRenderAsync browser width height StyleParam.ImageFormat.JPEG (gChart |> toFullScreenHtml)
@@ -129,10 +123,10 @@ type PuppeteerSharpRenderer() =
129123
member this.RenderJPG(width: int, height: int, gChart: GenericChart.GenericChart) =
130124
(this :> IGenericChartRenderer)
131125
.RenderJPGAsync(width, height, gChart)
132-
|> Async.RunSynchronously
126+
|> AsyncHelper.taskSync
133127

134128
member this.SaveJPGAsync(path: string, width: int, height: int, gChart: GenericChart.GenericChart) =
135-
async {
129+
task {
136130
let! rendered =
137131
(this :> IGenericChartRenderer)
138132
.RenderJPGAsync(width, height, gChart)
@@ -143,10 +137,10 @@ type PuppeteerSharpRenderer() =
143137
member this.SaveJPG(path: string, width: int, height: int, gChart: GenericChart.GenericChart) =
144138
(this :> IGenericChartRenderer)
145139
.SaveJPGAsync(path, width, height, gChart)
146-
|> Async.RunSynchronously
140+
|> AsyncHelper.taskSync
147141

148142
member this.RenderPNGAsync(width: int, height: int, gChart: GenericChart.GenericChart) =
149-
async {
143+
task {
150144
use! browser = fetchAndLaunchBrowserAsync ()
151145

152146
return! tryRenderAsync browser width height StyleParam.ImageFormat.PNG (gChart |> toFullScreenHtml)
@@ -155,10 +149,10 @@ type PuppeteerSharpRenderer() =
155149
member this.RenderPNG(width: int, height: int, gChart: GenericChart.GenericChart) =
156150
(this :> IGenericChartRenderer)
157151
.RenderPNGAsync(width, height, gChart)
158-
|> Async.RunSynchronously
152+
|> AsyncHelper.taskSync
159153

160154
member this.SavePNGAsync(path: string, width: int, height: int, gChart: GenericChart.GenericChart) =
161-
async {
155+
task {
162156
let! rendered =
163157
(this :> IGenericChartRenderer)
164158
.RenderPNGAsync(width, height, gChart)
@@ -169,10 +163,10 @@ type PuppeteerSharpRenderer() =
169163
member this.SavePNG(path: string, width: int, height: int, gChart: GenericChart.GenericChart) =
170164
(this :> IGenericChartRenderer)
171165
.SavePNGAsync(path, width, height, gChart)
172-
|> Async.RunSynchronously
166+
|> AsyncHelper.taskSync
173167

174168
member this.RenderSVGAsync(width: int, height: int, gChart: GenericChart.GenericChart) =
175-
async {
169+
task {
176170
use! browser = fetchAndLaunchBrowserAsync ()
177171

178172
let! renderedString =
@@ -184,10 +178,10 @@ type PuppeteerSharpRenderer() =
184178
member this.RenderSVG(width: int, height: int, gChart: GenericChart.GenericChart) =
185179
(this :> IGenericChartRenderer)
186180
.RenderSVGAsync(width, height, gChart)
187-
|> Async.RunSynchronously
181+
|> AsyncHelper.taskSync
188182

189183
member this.SaveSVGAsync(path: string, width: int, height: int, gChart: GenericChart.GenericChart) =
190-
async {
184+
task {
191185
let! rendered =
192186
(this :> IGenericChartRenderer)
193187
.RenderSVGAsync(width, height, gChart)
@@ -198,4 +192,4 @@ type PuppeteerSharpRenderer() =
198192
member this.SaveSVG(path: string, width: int, height: int, gChart: GenericChart.GenericChart) =
199193
(this :> IGenericChartRenderer)
200194
.SaveSVGAsync(path, width, height, gChart)
201-
|> Async.RunSynchronously
195+
|> AsyncHelper.taskSync

tests/Plotly.NET.ImageExport.Tests/ImageExport.fs

+6-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ let ``Image export tests`` =
2929
ptestAsync "Chart.toBase64JPGStringAsync" {
3030
let testBase64JPG = readTestFilePlatformSpecific "TestBase64JPG.txt"
3131

32-
let! actual = (Chart.Point([1.,1.]) |> Chart.toBase64JPGStringAsync())
32+
let! actual = (Chart.Point([1.,1.]) |> Chart.toBase64JPGStringAsync() |> Async.AwaitTask)
3333

3434
return
3535
Expect.equal
@@ -40,13 +40,17 @@ let ``Image export tests`` =
4040
ptestAsync "Chart.toBase64PNGStringAsync" {
4141
let testBase64PNG = readTestFilePlatformSpecific "TestBase64PNG.txt"
4242

43-
let! actual = (Chart.Point([1.,1.]) |> Chart.toBase64PNGStringAsync())
43+
let! actual = (Chart.Point([1.,1.]) |> Chart.toBase64PNGStringAsync() |> Async.AwaitTask)
4444

4545
return
4646
Expect.equal
4747
actual
4848
testBase64PNG
4949
"Invalid base64 string for Chart.toBase64PNGStringAsync"
5050
}
51+
testCase "Chart.toBase64JPGString" <| fun () ->
52+
let actual = Chart.Point([1.,1.]) |> Chart.toBase64JPGString()
53+
Expect.isTrue (actual.Length > 100) ""
54+
5155
]
5256
)

0 commit comments

Comments
 (0)