New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Road to Fable 2.0: Lightweight types? #1318
Comments
From all my use case, I never needed reflection when using Fable so for me it's a yes. After, perhaps we can still support reflection by using attributes if this is really needed. But in general I prefer avoiding reflection usage, I don't think it's a good practice and don't help in writing clean, robust code. In order to make the JS code more readable, we could generate comments like something: I am not sure to understand this part:
|
Not a good practice? Any kind of metaprogramming (outside of unsafe interop) will not be possible without Reflection and since Fable doesn't support qoutations, Reflection is the only way. Many library implementers using Reflection expect to be able to read the metadata of types provided by the users. |
@Zaid-Ajaj I just said that because, each time I use Reflection finally I switched back to another solution. :) Because, in general reflection usage is less frequent that's also why, I propose to support it via an attribute if we can't support it via another way. :) |
Some notes :)
|
Pojos is the winner for me as "compatibility with libraries like React" and debugging is a big plus. |
Bucklescript is a very good source for that kind of stuff.
One of the key selling point of Fable is the code readability, and someone that relies on this won't ever upgrade. In Babel we usually have some option to tune the output, what if Fable adds a compilation flag which turns the optimizations on. The downside is having to maintains two code paths doing basically the same thing, to avoid that I would recommend using another AST pass (if optimization enabled) to transform: In the case of the optimization being turned off, you could still debug or read the code as you currently can. |
|
Well I think it just summarizes my point of view. 😉
We need numbers 😉 Anyway, as long as Fable apps are easy to debug it will be ok for me because in the long run, it's always maintenance on old code which becomes pricey. |
@whitetigle Quoting @alfonsogarciacaro
And we have some work to remove around 100KB to Fulma, with a new version of Fable by manually adding |
Don't know if you've seen this but recent V8 versions have changed their approach to optimizing code: |
Hi all,
|
FWIW, I've only used reflection recently and only as a last resort. I'm only using it on a generic function (with 👍 on making compiled union / record types lighter |
If this is going to be controlled with a global switch, we probably need something to override this behavior per type, module or even project ( Would it make sense to introduce this feature in Fable 1.4 (?) for non-public types only and then extend it to all public APIs in Fable 2? |
Thanks a lot for the comments! They are very useful to make the design for Fable 2.0, I'll write a summary later 👍 Thanks also for the link @jgrund, the post was very good. About compilation flags for optimizations, I think this may increase the maintenance cost too much if it changes the runtime semantics which can make some functions in fable-core not work as expected. However, it should be possible to do some specific changes to make debugging easier: like using strings for union tags during development and integers in production. |
Performance is a feature and I think Fable should offer better runtime optimisations. Opt-in via attributes makes the code less readable and also harder to share with .net server side. Thus my preference would be compiler / project switches for optimisation features while retaining the old behaviour (at least until proven obsolete). Opt-in is still a good choice for selected application. I also thought that using F# arrays and lists could optionally be optimised to plain JS arrays for performance/size reasons. |
@alfonsogarciacaro performance is not that important so why bother, A wise man once told me dev experience is more important. 😝 Messing aside, reflection is the route of so much evil, you should wean people off it, if they need, they can stay on older version while they figure out a better way to implement without reflection. If it wasn't for reflection and a few other .net gems, we would be able to do much better flattening and inlining of execution graph in compiler (fsc). The project switches statements are going to be nightmare to maintain and split the project in two directions anyway, there is always the historical releases for those who need old way as an alternate version but I think you need to pick a direction and go with it. |
was that me? ;P Performance optimizations are important when anyone is actually using the project... and they actually have a problem with performance. I'd be very careful in introducing premature optimizations that may make the developer experience worse just for sake of some benchmarks. |
The main point of the "Lightweight types" isn't the performance. It's the bundle size if I understood my discussion with @alfonsogarciacaro . The performance, are just a consequence of this point :) In general, I rarely look at performance problems because it's kind of rare to see them. (I am just speaking of my experience :) ) |
@Krzysztof-Cieslak, well you have in some guise at some stage, but I was referring to @alfonsogarciacaro in this instance ... don't worry, your a wise man too. I fully agree, performance is important in library's where people using, some who will inevitably run into performance issues. I think the important balance is make the dev Api simple/elegant as possible and do all the hard performance work behind the scenes, its not a LOB app where we can/should have simple clear abstractions for complex logic ... It's a base library that should be magic and performant under the simple/elegant api, hiding complexities from Dev.
Due to CPU cache sizes and IO bandwidth, the two are highly correlated 😄 |
@gerardtoconnor Hehe, dev experience is very important, that's what we've been doing the past two years and thanks to that we now have awesome projects like Ionide, Elmish or Fulma. Now we can focus on performance 😉 Also now that we have a wider user base, we can see better how Fable is used on the wild and what features can we drop: these optimizations will mean you cannot do type testing on In any case, you're right about the difficulty to maintain different code generations, we'll have to define a clear path for Fable 2.0 instead. About reflection, Fable only allows a small part but it's useful for serialization and other tricks (like converting DUs into JS objects or dropdown options). This will be maintained (for types known at compile time or that use |
I am passing objects as messages between Fable and Akka.Net/F# on the server. Because the type of the message identifies the message, being able to include the type in the serialization and being able to deserialize something in Fable without knowing what the type might be, will remain important to me. After deserialization of a message on the client, its runtime type :? (instanceof) is used to route the message. |
I just reached the 1MB size for prod bundle... Now, it is OK for high speed internet but not ok for anything other than that. And again it is indeed a big size. So, if anything coming as reducing the size. I am up for it. Just to give idea. Where I can't even use type Validate = {
IsValid : bool
ErrMsg : string
} with
static member isValid =
(fun m -> m.IsValid), (fun n m -> {m with IsValid = n})
static member errMsg =
(fun m -> m.ErrMsg), (fun n m -> {m with ErrMsg = n})
type DocumentStatus = Select = 0 | All = 1 | New = 2 | InProcess = 3 | Processed = 4 | Error = 5
type DocumentModel = {
File : string
FileName : string
Label : string
Status : DocumentStatus
Patient : string
Date : string
Notes : string
}with
static member file = (fun m -> m.File), (fun n m -> {m with File = n})
static member fileName = (fun m -> m.FileName), (fun n m -> {m with FileName = n})
static member label = (fun m -> m.Label), (fun n m -> {m with Label = n})
static member status = (fun m -> m.Status), (fun n m -> {m with Status = n})
static member patient = (fun m -> m.Patient), (fun n m -> {m with Patient = n})
static member date = (fun m -> m.Date), (fun n m -> {m with Date = n})
static member note = (fun m -> m.Notes), (fun n m -> {m with Notes = n})
type ErrorModel = {
File : Validate
FileName : Validate
Label : Validate
Patient : Validate
Date : Validate
Notes : Validate
}with
static member file = (fun m -> m.File), (fun n (m:ErrorModel) -> {m with File = n})
static member fileName = (fun m -> m.FileName), (fun n (m:ErrorModel) -> {m with FileName = n})
static member label = (fun m -> m.Label), (fun n (m:ErrorModel) -> {m with Label = n})
static member patient = (fun m -> m.Patient), (fun n (m:ErrorModel) -> {m with Patient = n})
static member date = (fun m -> m.Date), (fun n (m:ErrorModel) -> {m with Date = n})
static member notes = (fun m -> m.Notes), (fun n (m:ErrorModel) -> {m with Notes = n})
type Model = {
Id : int
ShowUploadModal : bool
DocumentModel : DocumentModel
ErrorModel : ErrorModel
IsValid : bool
}with
static member documentModel = (fun m -> m.DocumentModel),(fun n m -> {m with DocumentModel = n})
static member errorModel = (fun m -> m.ErrorModel), (fun n m -> {m with ErrorModel = n}) Now this gonna increase size like anything. Static functions are there to use And obviously many fish operators are missing for readability... |
@kunjee17 You can also not use static member to have the lenses support. You can write them like: type Model = {
Id : int }
}
let modelIdLens = ...
// or
module ModelLens =
let id = ... And so be able to use the Do you use CDN, in your application or are you bundling everything in it like your code and libraries too ? |
@MangelMaxime I am bundling everything as of now. Here are the libraries I am using as of now
I ll give a shot at what you said but putting things in module instead of static member Update1 For above code here is generated code
And here is updated code
it will generate more JavaScripty code
So, what will be good route to take. Is this thing can be default in fable ? @alfonsogarciacaro |
Update2 I am half way through modifying all models with Pojo . It is reducing the code; But seems so little. Will keep posting on update. |
I think it would be better to keep this discussion in a separate issue @kunjee17 just try keeping this one a discussion :) |
@MangelMaxime sorry got carried away. But yes we need light weight types or a guidelines to achieve that... |
@michaelsg I think we had a similar discussion when releasing Fable 0.7 or 1.0. To support deserialization with type info we added a global variable to hold a type dictionary. This is the only global variable in fable-core and I'd like to remove it for the next version, as I haven't heard of anybody else using this feature. Generics will still be available so you could do something like the following to tag your JSON with the typename: let inline toJsonWithTypeName (o: 'T) =
toJson (typeof<'T>.FullName, o) // On server side you would use Newtonsoft.Json
let ofJsonWithTypeName json =
let (typeName, parsed): string * obj = ofJson json
if typeName = typeof<MyType1>.FullName then
let x = inflate<MyType1> parsed
// Do something with MyType1
()
elif typeName = typeof<MyType2>.FullName then
let x = inflate<MyType2> parsed
// Do something with MyType2
()
else
failwithf "Cannot handle type %s" typeName This code is not very elegant (probably we can improve it using Active Patterns or a type dictionary) but it's a small price to pay to improve the rest of your (and everybody else's) generated code. As Fable apps are growing we're noticing that the bundles are getting quite big so we need to remove some runtime info in order to remain competitive agains alternate solutions (like Reason).
Two other notes: unions and records will be plain objects so you will be able to parse them just with the native browser API @kunjee17 Yes, it's better to move the discussion to Fulma or open a new issue :) Please check the size of the gzipped bundle as that would be the size to download if your server gzips JS files, which should be doing ;) Fable 2.0 will improve the situation but if your bundle is growing too much at one point you may need to split your app. |
@alfonsogarciacaro I am afraid the property inlining will not help Fable2 here as it looks like the property itself will introduces lots of additional operations and indirection (and will trash the data and instruction cache) instead of a simple object field access:(https://gist.github.com/zpodlovics/a454ad7521945f367bc13b69a7230118#file-fable2-profile-txt-L362). And inlining is not always a win - as you have to execute usually lot more code - instead of using a call reusing the hot instruction & uop cache. Small, tight code (eg.: K interpreter) could be really-really fast: (http://tech.marksblogg.com/billion-nyc-taxi-kdb.html). Property related new functionality from the Fable2 profile:
A few property related functionality overhead from the Fable2 profile:
A few % here and another few % there and this small list suddenly responsible for: ~22.6% |
Thanks for the new data @zpodlovics. To be sure I'm understanding it well, is this the change in the generated JS code which is causing the performance issue? // Fable 1
class Foo {
get MyProperty() {
return costlyOperation(this);
}
}
x.MyProperty // Use example
// Fable 2
function Foo$$get_MyProperty(this$) {
return costlyOperation(this$);
}
Foo$$get_MyProperty(x) // Use example I was hoping that using a function (besides better compatibility with JS tree shaking and minification) would help the JS runtime to make a static dispatch, but maybe I was wrong. However, in both cases In any case I'm open to attach properties again to the prototype if this can improve performance. If we create a branch with this change, would you test it in your project? |
The performance hit - according to my profiling - comes from the Fable1 object -> Fable2 property change. There are no costly operation in the encoding/decoding path. Each call is nothing more than a primitive type read/write (+ sometimes a few ( < 100 ) byte array read/write) + minimal transformation (usually integer bitness expansion / truncation) if needed. However there are lot's of fields in an encoder/decoder (not only primitive types, but structures (including nested structures and variable length data) that built from these primitive types). The encoder/decoder allocated once (with nested encoder/decoder for each non-primitive field/structure), so the message encoding/decoding could be done without allocation in the hot path. A trivial header decoder/encoder with two field: Fable1 generated js: Fable2 generated js: Flamegraphs (using the earlier benchmarks - is there any better way than this to share svg?): Fable1 flamegraph: Fable2 flamegraph: The field read/write operation must evaluated every time as expected. In a concurrent environment (js shared buffers + workers) not even the local cpu caching are allowed, the read/write done using the physical memory (volatile ops). Thank you for your support! I am happy to help to improve Fable, so please create the branch and I will test it in my project. Update: |
@alfonsogarciacaro Based on my earlier updates (Mathias Bynens posts + conference videos) it seems that the performance hit comes from the multiple shape definition and the shape walking (plus the additional indirection introduced by the standalone functions?). Also the defineProperty usage also seems problemmatic. In theory V8 have some inline cache tracing functionality (at least in debug mode const object1 = { x: 1, y: 2 }; vs const object = {};
object.x = 5;
object.y = 6; Probably also worth to check this: |
@zpodlovics I'm not very used to JS profiling tools but I was aware that it's better to avoid polymorphism to help the JS engine optimize the code. However, I was hoping that the standalone functions could be optimized because they're used with the same type. In any case, I've created the Maybe @ncave can also give it a go? |
@alfonsogarciacaro My build fails with some Map/Set test error:
Full error list: https://gist.github.com/zpodlovics/a19bbfba7c1d47796c160604af3e90f3 Update: commented out the tests, now fighting with #1413 |
Sorry, I'm an idiot. Forgot to rebuild fable-core before running the tests. Let me fix this. |
Still waiting for CI to finish, but hopefully it's fixed now 🙏 |
Tests are passing but the REPL is not building :/ Anyways, can you please give it a go @zpodlovics? If it works in your case and shows a performance improvement, we can work on top of it to fix the remaining issues. |
@alfonsogarciacaro Thank you! I have managed to get it working (now it's based on fable-core.2.0.0-beta-002). While the code looks different, the performance roughly remains the same. I am afraid, there is no other way than dig deeper using the js profiling tools.
https://gist.github.com/zpodlovics/266d35c2064af1ecad1733ecb21a2d56 |
@alfonsogarciacaro Your generic parameters / interface explanation here (#1547 (comment)) gave me an idea to try out without any interface related "magic" with Fable2: In the codec I have replaced the interface type with a concrete implementation type, and viola:
|
Oh really cool @zpodlovics. If I read correctly you even have performance improvement here. Can I ask you to share an example of code before / after related to your point:
Like that we can refer to it later, and perhaps others people can find it useful "trick" :) |
@MangelMaxime Nothing really, just replaced the interface type with a concrete type (but you have to do more complex things than this to measure the call overhead correctly). [<Interface>]
type IFunc1 =
abstract member Func1: int32 -> int32
type MyFunc1() =
member this.Func1(x:int32) = x+2
interface IFunc1 with
member this.Func1(x: int32) = this.Func1(x)
[<Class>]
type TestInterface(instance: IFunc1) =
member this.Call(x: int32) =
instance.Func1(x)
type TestGeneric<'T when 'T:> IFunc1>(instance: 'T) =
member this.Call(x: int32) =
instance.Func1(x)
type TestSpecialized(instance: MyFunc1) =
member this.Call(x: int32) =
instance.Func1(x)
let myF = MyFunc1()
let myCI = TestInterface(myF)
let myCG = TestGeneric(myF)
let myCS = TestSpecialized(myF) |
Interesting, dynamic dispatch rears its ugly head :) |
@et1975 @realvictorprm Inline interfaces to the rescue: fsharp/fslang-suggestions#641 and dotnet/fsharp#4726 |
Thanks for investigating this further @zpodlovics, so the performance loss comes from interface casting (in Fable 1 there was no casting as interface members were attached directly to the object) instead of the standalone properties. This is somewhat expected but we need to make users aware of this. Another alternative could be to use object expressions to implement the interface instead of a type, these don't need casting. |
Is it casting or dynamic dispatch? Seems the distinction would be important to know. |
@et1975 It's more like shape creation and shape migration/transformation - at least based on the profiling /flamegraph. |
Yes, we don't have something like virtual tables, but Fable 2 transforms (actually wraps) objects when casted to interfaces. As this operation is implicit when passing an object to a function accepting the interface it may be that it happens too many times hurting performance. Maybe we can find a way to detect these cases and either find a workaround or inform the user. |
Would it be possible to make it like a lazy value and calculated just once? |
Great idea @Nhowka! I've created an |
@alfonsogarciacaro It looks roughly the same:
The flamegraph also changed a bit: |
Hmm, so maybe the cost of using a map outweighs the benefit of having a cache. Well, the important thing is we have identified a potential bottleneck so we can write it in the documentation and then users can try changing interfaces by concrete types as in your case. @ncave Maybe we could check with REPL in case it makes some difference there. I've fixed the bundle, but the bench project probably needs fixing too. |
@alfonsogarciacaro It seems that any indirection introduced will trash the performance in a call heavy environment. However rewriting - by hand - lot's of existing and future shared (.NET + Fable) code is not really a solution. Generic type parameters / flexible types should allow this kind of specialization (roughly the same way .NET do): |
Update: the new No interface (interface names replaced with the concrete implementation type):
perf stat:
Interface (type TestInterface(instance: IFunc1) style) using the attach-interfaces branch:
perf stat:
|
@alfonsogarciacaro Can we close this issue ? |
One of the guidelines when designing Fable was to generate JS as standard as possible that could be easily consumed from other JS code. Because of this F# types (including records and unions) were compiled to the closest thing to real types in modern JS: ES2015 classes (with reflection info).
This is working well and it also means you can export the type to JS and call normally all properties and methods. However, it also means types become somewhat expensive (mainly in terms of code size) in JS, where it's more usual to define less classes and use more literal objects (aka, plain old JS objects or pojo). Example where 2 lines of F# result in 36 lines of JS. It's becoming particularly obvious in Elmish apps where you usually declare lots of small types to define messages and models. Fulma is an example of a very useful library that adds many KBs to the app bundle, which can be a big drawback for some users.
To counter this we are trying to optimize the compilation of certain types with a special attribute (see #1308 #1316). This is giving good results but it forces the user to remember to use the attribute and its special rules, and also to tolerate slightly different runtime semantics for the same type depending on the decorator (and we already have too many such attributes!
Erase
,Pojo
...). A different approach for a new major version would be to compile all records and unions in a more lightweight-fashion so the semantics are more predictable and every user gets the benefits without using special attributes. An initial proposal could be to do something similar to what Reason/Bucklescript are doingBar(6, "foo")
becomes[0, 6, "foo"]
(the first element is the tag index). Unions without any data field (e.gtype Foo = A | B | C
) could be compiled as integers (same as enums). This is what Reason/Bucklescript do and given how often unions are used in F# programs to model messages, etc, it should shave a lot of code as array syntax is very lightweight in JS, as well as performance gains (JS engines are very optimized to deal with arrays).Notice this means there won't be a class/function being generated in JS to represent the type. Instance and static members of the type will become methods of the enclosing module. Actually, this is what the F# compiler does by default, but Fable mounts the members in the type.
This will take a considerable amount of work and will be incompatible with some of the current Fable features, specially around reflection and type testing. We have to decide carefully if the pros outweigh the cons:
Pros
Cons
The text was updated successfully, but these errors were encountered: