-
Notifications
You must be signed in to change notification settings - Fork 588
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
[PERF] large number of duplicated hard/soft references kills the algorithm #2036
Comments
Yes we at work have a use-case as well where we dynamically create targets and indeed we noticed a performance degradation where fake calculates something for like a minute before starting the script (with like >50 targets) so any fix/pr is welcome ;). This is just another case on top of that but yes the stuff is way slower than it needs to be - I guess there are faster algorithms available... |
Hm I'm not sure I fixed this with the linked commit (not sure why) but I cannot reproduce this with the following test: type TestTarget =
| TestTarget of string
| TestTargetGroup of TestTarget list
member x.TargetNames = seq {
match x with
| TestTarget s -> yield s
| TestTargetGroup g -> yield! g |> Seq.collect (fun g -> g.TargetNames)
}
module TestTarget =
let create name body =
Target.create name body
TestTarget name
//test
Fake.ContextHelper.fakeContextTestCase "basic performance #2036" <| fun _ ->
// Increase counter to 1000 and run with dotTrace
let counter = 1000
let all_Pipelines = Dictionary<string,TestTarget>(System.StringComparer.OrdinalIgnoreCase :> IEqualityComparer<string>)
let soft = HashSet()
let hard = HashSet()
// set this to TRUE to add duplicated references to FAKE.
let degradeAlgo = true
/// The last target is the name of the pipeline
let SetPipelineRelations (targets:TestTarget list) : unit =
let targetNames = targets |> Seq.collect (fun t -> t.TargetNames)
let last = targetNames |> Seq.last
all_Pipelines.[last] <- TestTargetGroup targets
for (a,b) in targetNames |> Seq.pairwise do
if soft.Add(a,b) || degradeAlgo then
a ?=> b |> ignore
if hard.Add(a,last) || degradeAlgo then
a ==> last |> ignore
let CreatePipeline name (targets: TestTarget list) : unit =
let p = TestTarget.create name ignore
SetPipelineRelations [ yield (TestTargetGroup targets); yield p ]
let t_targets =
TestTargetGroup [
for i in 0 .. counter ->
TestTarget.create (sprintf "Target_%i" i) ignore
]
[ t_targets ] |> CreatePipeline "Run.1"
Target.runOrDefaultWithArguments "Run.1" which is running in 2 seconds. Unfortunately looking at the code: let internal checkIfDependencyCanBeAddedCore fGetDependencies targetName dependentTargetName =
let target = get targetName
let dependentTarget = get dependentTargetName
let rec checkDependencies dependentTarget =
fGetDependencies dependentTarget
|> List.iter (fun dep ->
if String.Equals(dep, targetName, StringComparison.OrdinalIgnoreCase) then
failwithf "Cyclic dependency between %s and %s" targetName dependentTarget.Name
checkDependencies (get dep))
checkDependencies dependentTarget
target,dependentTarget I still think this is not really optimal because if might go through the same targets multiple times but if I cannot reproduce I'll probably leave it unchanged rather than introducing bugs. |
With |
Edit: ah, the issue is not duplicated refs, but just a large number, so that doesn't help. Sorry. I peeked at the PR, and this still adds the duplicated references, right? It just improves the scanning algorithm. The list could be replaced with a HashSet of |
@0x53A Thanks for the tip, yes there are a lot of ways to improve this. Everything is implemented in a very very naive way. I'm trying to figure out what you mean after I managed to make the stuff tail recursive (to no longer stack-overflow) Any idea why let rec visitDependenciesAux fGetDependencies previousDependencies = function
| (visited, level, targetName) :: workLeft ->
let target = get targetName
let isVisited =
visited
|> Seq.exists (fun t -> String.Equals(t, targetName, StringComparison.OrdinalIgnoreCase))
if isVisited then
visitDependenciesAux fGetDependencies previousDependencies workLeft
else
let deps =
fGetDependencies target
|> Seq.map (fun (t) -> (String.toLower targetName::visited), level + 1, t)
|> Seq.toList
let newVisitedDeps = target :: previousDependencies
visitDependenciesAux fGetDependencies newVisitedDeps (deps @ workLeft)
| _ -> previousDependencies |> List.rev Is not considered tail recursive by the compiler (I hate that part of F#) |
It works when I remove the |
No clue. F# really needs a You could disassemble both variants and compare them. Also, afaik the jitter sometimes ignores the Async has trampolining, right? So another quick-fix with a slight performance penalty could have been to just wrap it in Then execute it with I'm not 100% sure, but I think that should also take care of stackoverflows. |
@0x53A Just to confirm. Is that what you suggest (ie check the data before adding stuff)? /// Adds the dependency to the front of the list of dependencies.
/// [omit]
let internal dependencyAtFront targetName dependentTargetName =
let target,_ = checkIfDependencyCanBeAdded targetName dependentTargetName
let hasDependency =
target.Dependencies
|> Seq.exists (fun d -> String.Equals(d, dependentTargetName, StringComparison.OrdinalIgnoreCase))
if not hasDependency then
getTargetDict().[targetName] <-
{ target with
Dependencies = dependentTargetName :: target.Dependencies
SoftDependencies =
target.SoftDependencies
|> List.filter (fun d -> not (String.Equals(d, dependentTargetName, StringComparison.OrdinalIgnoreCase)))
}
/// Appends the dependency to the list of soft dependencies.
/// [omit]
let internal softDependencyAtFront targetName dependentTargetName =
let target,_ = checkIfDependencyCanBeAdded targetName dependentTargetName
let hasDependency =
target.Dependencies
|> Seq.exists (fun d -> String.Equals(d, dependentTargetName, StringComparison.OrdinalIgnoreCase))
let hasSoftDependency =
target.SoftDependencies
|> Seq.exists (fun d -> String.Equals(d, dependentTargetName, StringComparison.OrdinalIgnoreCase))
match hasDependency, hasSoftDependency with
| true, _ -> ()
| false, true -> ()
| false, false ->
getTargetDict().[targetName] <- { target with SoftDependencies = dependentTargetName :: target.SoftDependencies }
|
At least there seems to be no more significant difference between |
Yes that is what I meant. This looks like a great improvement, in my original repro just Thank you! |
* Fixes various Stackoverflows by making stuff tail recursive * fix traverse algorithms by using visited hashsets * Make sure to skip duplicated dependencies * Other improvements seen in the profiler (ie String.Equals instead of toLower) Fixes #2036
EDIT:
Repro is here:
https://github.com/0x53A/fake-loop
You can increase the complexity in
with 8 it takes 3 seconds, with 9, it takes 16 seconds for me. 10 didn't finish after a few minutes.
You can fix it by setting
in
_targetEx.fsx
, then it will deduplicate the references on the script side.The text was updated successfully, but these errors were encountered: