Last active
December 27, 2015 21:48
-
-
Save swlaschin/10558434 to your computer and use it in GitHub Desktop.
F# scripts for analysis of Roslyn vs F# compiler
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(* | |
This script counts lines in C# and F# projects. | |
It is used to compare the "useful lines" in F# projects with those in C# projects. | |
Copied from Kit Eason's code at http://www.fssnip.net/h4 | |
REQUIRES: | |
* FSharp.Charting for charts | |
via NuGet | |
Install-Package FSharp.Charting | |
USING NUGET | |
NuGet requires a project, so I just create an empty project and solution and add this script to it. | |
Once the project exists, you can run the NuGet commands from the "Package Manager Console" . | |
*) | |
// make Visual Studio use the script directory | |
System.IO.Directory.SetCurrentDirectory(__SOURCE_DIRECTORY__) | |
#load "packages/FSharp.Charting.0.90.5/FSharp.Charting.fsx" | |
open System | |
open System.IO | |
open System.Text.RegularExpressions | |
open FSharp.Charting | |
open System.Windows.Forms | |
/// Enumerate files starting at a given path, having a given wildcard. | |
/// (Inaccessible directories are ignored.) | |
let safeEnumerateFiles (path : string) (wildcard: string) = | |
let safeEnumerate f path = | |
try | |
f(path) | |
with | |
| :? System.UnauthorizedAccessException -> Seq.empty | |
let enumerateDirs = | |
safeEnumerate Directory.EnumerateDirectories | |
let enumerateFiles = | |
safeEnumerate (fun path -> Directory.EnumerateFiles(path, wildcard)) | |
let rec enumerate baseDir = | |
seq { | |
yield! enumerateFiles baseDir | |
for dir in enumerateDirs baseDir do | |
yield! enumerate dir | |
} | |
enumerate path | |
/// Return all the lines in files matching the given wildcard starting at the given path. | |
let fileLines path wildcard = | |
safeEnumerateFiles path wildcard | |
|> Seq.map (fun fileName -> File.ReadAllLines(fileName)) | |
|> Seq.concat | |
/// Break down the sequence giving counts of lines matching each regex pattern. | |
let countByPattern patterns lines = | |
seq { for pattern in patterns do | |
yield lines | |
|> Seq.filter (fun line -> Regex.IsMatch(line, pattern)) | |
|> Seq.length | |
} | |
/// Break down the given lines into counts by pattern, associating each count with a label. | |
let analyze patternsLabels lines = | |
let patterns, labels = | |
patternsLabels |> Array.map fst, | |
patternsLabels |> Array.map snd | |
lines | |
|> countByPattern patterns | |
|> Seq.zip labels | |
/// Analyze C# files starting at the given path, showing various forms of noise. | |
let analyzeCSharp path = | |
let lines = fileLines path "*.cs" |> Seq.cache | |
let patternCounts = | |
lines | |
|> analyze [| | |
"(^([ \t]*)([{}])([ \t]*)$)", "{ or }" | |
"(^[ \t]*$)", "Blank" | |
"(\=[ \t]*null)", "Null usages" | |
"(^[ \t]*//)", "Comments" | |
|] | |
let otherCount = ["Useful lines", (lines |> Seq.length) - (patternCounts |> Seq.sumBy snd)] | |
Seq.append patternCounts otherCount | |
|> Array.ofSeq | |
let analyzeFSharp path = | |
let lines = fileLines path "*.fs" |> Seq.cache | |
let patternCounts = | |
lines | |
|> analyze [| | |
"(^([ \t]*)([{}])([ \t]*)$)", "{ or }" | |
"(^[ \t]*$)", "Blank" | |
"(\=[ \t]*null)", "Null checks" | |
"(^[ \t]*//)", "Comments" | |
|] | |
let otherCount = ["Useful lines", (lines |> Seq.length) - (patternCounts |> Seq.sumBy snd)] | |
Seq.append patternCounts otherCount | |
|> Array.ofSeq | |
/// Show an array of strings and ints as a labelled pie chart. | |
let toPie name (results : array<string*int>) = | |
let chart = Chart.Pie(results, Name = name) | |
chart.ShowChart() | |
chart.SaveChartAs(name+".png",ChartTypes.ChartImageFormat.Png) | |
// calculate the number of LOC in each file | |
let linesPerFile path wildcard = | |
let classify = function | |
| lines when lines <= 200 -> 0,200 | |
| lines when lines <= 300 -> 201,300 | |
| lines when lines <= 500 -> 301,500 | |
| lines when lines <= 800 -> 501,800 | |
| lines when lines <= 1300 -> 801,1300 | |
| lines when lines <= 2100 -> 1301,2100 | |
| lines when lines <= 3400 -> 2101,3400 | |
| lines when lines <= 5500 -> 3401,5500 | |
| _ -> 5501,10000 | |
let add map key = | |
match Map.tryFind key map with | |
| Some count -> Map.add key (count+1) map | |
| None -> Map.add key 1 map | |
let formatBounds (lower,upper) = | |
if lower <= 5500 then | |
sprintf "%i-%i" lower upper | |
else "> 5500" | |
safeEnumerateFiles path wildcard | |
|> Seq.map (fun fileName -> File.ReadAllLines(fileName).Length) | |
|> Seq.map classify | |
|> Seq.fold add Map.empty | |
|> Map.toList | |
|> List.sortBy (fun (key,v) -> key) | |
|> List.map (fun (k,v) -> formatBounds k, v) | |
// ================================ | |
// Roslyn projects | |
// ================================ | |
let roslynPath = @"P:\git_repos\roslyn\Src\Compilers\Core\Source" | |
let roslynAnalysis = analyzeCSharp roslynPath | |
let roslynLineCount = fileLines roslynPath "*.cs" |> List.ofSeq |> List.length | |
let roslynFileCount = safeEnumerateFiles roslynPath "*.cs" |> List.ofSeq |> List.length | |
let roslynLinesPerFile = linesPerFile roslynPath "*.cs" | |
toPie "roslyn" roslynAnalysis | |
let roslynCSharpPath = @"P:\git_repos\roslyn\Src\Compilers\Csharp\Source" | |
let roslynCSharpAnalysis = analyzeCSharp roslynCSharpPath | |
let roslynCSharpLineCount = fileLines roslynCSharpPath "*.cs" |> List.ofSeq |> List.length | |
let roslynCSharpFileCount = safeEnumerateFiles roslynCSharpPath "*.cs" |> List.ofSeq |> List.length | |
let roslynCSharpLinesPerFile = linesPerFile roslynCSharpPath "*.cs" | |
toPie "roslynCsharp" roslynCSharpAnalysis | |
// ================================ | |
// F# Compiler project | |
// ================================ | |
// Moved all compiler-only files from to a subdirectory to separate them from tests, etc. | |
// Fsharp.Core was explicitly excluded-- this is a shared libary that is not compiler specific | |
let fsharpCompilerPath = @"P:\git_repos\fsharp\src\fsharp\CompilerSource" | |
let fsharpCompilerAnalysis = analyzeFSharp fsharpCompilerPath | |
let fsharpCompilerLineCount = fileLines fsharpCompilerPath "*.fs" |> List.ofSeq |> List.length | |
let fsharpCompilerFileCount = safeEnumerateFiles fsharpCompilerPath "*.fs" |> List.ofSeq |> List.length | |
let fsharpCompilerLinesPerFile = linesPerFile fsharpCompilerPath "*.fs" | |
toPie "fsharpCompiler" fsharpCompilerAnalysis | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(* | |
This script analyzes the dependencies between top level types in a .NET Assembly. | |
It is then used to compare the dependency relationships in some F# projects with those in some C# projects. | |
Note that no attempt has been made to optimize the code yet! | |
REQUIRES: | |
* Mono.Cecil for code analysis | |
From http://www.mono-project.com/Cecil#Download | |
or via NuGet | |
Install-Package Mono.Cecil -Version 0.9.5.4 | |
* QuickGraph for graph algorithm | |
or via NuGet | |
Install-Package QuickGraph -Version 3.6.61119.7 | |
* GraphViz for graph rendering. | |
From http://www.graphviz.org/Download.php | |
Alternatives are: GLEE (http://research.microsoft.com/en-us/downloads/f1303e46-965f-401a-87c3-34e1331d32c5/default.aspx) | |
or Canviz (https://code.google.com/p/canviz/) to render DOT files with JS in the browser. | |
USING NUGET | |
NuGet requires a project, so I just create an empty project and solution and add this script to it. | |
Once the project exists, you can run the NuGet commands from the "Package Manager Console" . | |
*) | |
// make Visual Studio use the script directory | |
System.IO.Directory.SetCurrentDirectory(__SOURCE_DIRECTORY__) | |
#r @"packages\Mono.Cecil.0.9.5.4\lib\net40\Mono.Cecil.dll" | |
#r @"packages\Mono.Cecil.0.9.5.4\lib\net40\Mono.Cecil.Rocks.dll" | |
#r @"packages\QuickGraph.3.6.61119.7\lib\net4\QuickGraph.dll" | |
open System | |
open System.IO | |
// ================================ | |
// The domain types - note there are no dependencies on Cecil | |
// ================================ | |
/// Distinguish a Type name from normal strings. | |
/// Use a "name" rather than a Cecil.TypeDefinition/TypeReference to satisfy the comparison constraint of sets | |
type TypeName = TypeName of string | |
/// Distinguish top level types from normal types | |
type TltName = TltName of string | |
/// a dependency to another type can be internal (same assembly) or external (different assembly) | |
type TypeDependency = | |
| Internal of TypeName | |
| External of TypeName | |
/// a type with its dependencies -- references exposed publically and references used in the implementation | |
type TypeWithDependencies = | |
{ typeDef: TypeName; isAuthored: bool; publicDeps: TypeDependency Set; implDeps: TypeDependency Set} | |
/// a top level class/module along with all the types it contains | |
type TopLevelType = | |
{ name: TltName; types: TypeWithDependencies Set} | |
/// a top level class/module along with the top level types it depends on | |
type DependencySet = | |
{ dependent: TltName; dependencies: TltName Set} | |
// ================================ | |
// Module for extracting types from an assembly (using Cecil) | |
// ================================ | |
module AssemblyTypes = | |
open Mono.Cecil | |
/// Extract the full name from the TypeDefinition or TypeReference | |
let inline typeName td = TypeName ( ^a : (member FullName : string) td) | |
/// A type is "core" if it is in the Microsoft.FSharp namespace | |
/// This stops projects with Fsharp.Core built in from double counting | |
let isCoreName (s:string) = | |
s.StartsWith("Microsoft.FSharp") | |
// used to filter out types that should not be included | |
let isNotCoreType excludeCore (td:TypeDefinition) = | |
not (isCoreName td.FullName && excludeCore) | |
/// get a named attribute of the specified type, or None | |
let getCustomAttribute (td:TypeDefinition) attrName = | |
td.CustomAttributes | |
|> Seq.tryFind (fun a -> a.AttributeType.FullName = attrName ) | |
/// get a named attribute constructor arg of the specified type, or None | |
let getCustomAttributeArg (td:TypeDefinition) attrName argName = | |
getCustomAttribute td attrName |> Option.bind (fun attr -> | |
attr.ConstructorArguments |> Seq.tryFind (fun arg -> arg.Type.FullName = argName ) | |
) | |
// is the type a F# sum type? | |
let isFsSumType (td:TypeDefinition) = | |
let attrName = "Microsoft.FSharp.Core.CompilationMappingAttribute" | |
let argName = "Microsoft.FSharp.Core.SourceConstructFlags" | |
getCustomAttributeArg td attrName argName | |
|> Option.map (fun arg -> arg.Value = box (int SourceConstructFlags.SumType)) | |
|> defaultArg <| false | |
// is the type a case in a F# sum type? | |
let isFsCaseType (td:TypeDefinition) = | |
(td.DeclaringType <> null) && (isFsSumType td.DeclaringType) | |
// does the type have the GeneratedCodeAttribute? | |
let hasGeneratedCodeAttribute (td:TypeDefinition) = | |
let attrName = "System.CodeDom.Compiler.GeneratedCodeAttribute" | |
getCustomAttribute td attrName |> Option.isSome | |
/// Return true if the type name was generated by the compiler | |
let hasGeneratedTypeName (s:string) = | |
s.Contains("__") // C# | |
|| s.Contains("<") // C# | |
|| s.Contains("@") // F# | |
|| s.Contains("$") // F# | |
/// Return true if the type was authored by a human | |
let isAuthored td = | |
(td |> hasGeneratedCodeAttribute |> not) && | |
(td |> isFsCaseType |> not) && | |
(td.FullName |> hasGeneratedTypeName |> not) | |
/// a filter for nulls | |
let isNotNull o = (o<>null) | |
/// True if type reference is external to the dependant | |
/// This is caused by it being in a different assembly, or if excludeCore is true, by being in the Microsoft namespace | |
let isExternalRef (dependant:TypeDefinition) excludeCore (tr:TypeReference) = | |
// hack to handle nulls | |
let trScopeName = | |
if tr = null then "" | |
else if tr.Scope = null then "" | |
else tr.Scope.Name | |
match dependant.Scope.Name with | |
| s when s <> trScopeName -> true | |
| s when excludeCore && isCoreName s -> true | |
| _ -> false | |
/// Make a dependency from a TypeReference | |
let toDependency isExternal (tr:TypeReference) = | |
if isExternal tr | |
then typeName tr |> External | |
else typeName tr |> Internal | |
/// Given a TypeReference, return it and its generic arguments as well (recursively) | |
/// E.g IList<Option<String>> expands to { IList<>; Option<>; String } | |
let rec refWithGenericParams (tr:TypeReference) = seq { | |
match tr with | |
| :? GenericInstanceType as g -> | |
yield tr | |
for arg in g.GenericArguments do yield! refWithGenericParams arg | |
| _ -> | |
yield tr // just the typeRef | |
} | |
/// Get a list of all types directly referenced by public elements of a TypeDefinition | |
/// Not expanded to include generic params | |
let directPublicRefs (td:TypeDefinition) = seq { | |
yield td.BaseType | |
for int in td.Interfaces do | |
yield int | |
for fd in td.Fields do | |
if fd.IsPublic then yield fd.FieldType | |
// properties are included in methods | |
for md in td.Methods do | |
if md.IsPublic then | |
for parmd in md.Parameters do yield parmd.ParameterType | |
yield md.ReturnType | |
} | |
/// Get a list of all types directly or indirectly referenced by public elements of a TypeDefinition | |
let publicRefs (td:TypeDefinition) = | |
if td.IsPublic | |
then | |
td |> directPublicRefs |> Seq.collect refWithGenericParams | |
else | |
Seq.empty // nothing for private types | |
/// Create a visitor interface for the Cecil.Rocks.ILParser | |
/// It accumulates type references in a typeRefs param | |
let implVisitor typeRefs = | |
{new Mono.Cecil.Rocks.IILVisitor with | |
member this.OnInlineNone (_) = () | |
member this.OnInlineSByte (_, _) = () | |
member this.OnInlineByte (_, _) = () | |
member this.OnInlineInt32 (_, _) = () | |
member this.OnInlineInt64 (_, _) = () | |
member this.OnInlineSingle (_, _) = () | |
member this.OnInlineDouble (_, _) = () | |
member this.OnInlineString (_, _) = () | |
member this.OnInlineBranch (_, _) = () | |
member this.OnInlineSwitch (_, _) = () | |
member this.OnInlineSignature (_, _) = () | |
member this.OnInlineVariable (_, variable) = | |
if variable <> null then | |
typeRefs := variable.VariableType :: !typeRefs; () | |
member this.OnInlineArgument (_, parameter) = | |
if parameter <> null then | |
typeRefs := parameter.ParameterType :: !typeRefs; () | |
member this.OnInlineType (_, t) = | |
if t <> null then | |
typeRefs := t :: !typeRefs; () | |
member this.OnInlineField (_, field) = | |
if field <> null then | |
typeRefs := field.DeclaringType :: !typeRefs; () | |
member this.OnInlineMethod (_, m)= | |
if m <> null then | |
typeRefs := m.DeclaringType :: !typeRefs; () | |
} | |
/// Get a list of all types directly referenced by the implementation of a method | |
let directMethodRefs (md:MethodDefinition) = | |
let typeRefs = ref [] | |
let visitor = implVisitor typeRefs | |
try | |
Mono.Cecil.Rocks.ILParser.Parse(md,visitor) | |
with | |
| ex -> () // ILParser can throw NRE -- ignore for our purposes | |
!typeRefs | |
/// Get a list of all types directly referenced by the implementation of all methods of a TypeDefinition | |
/// Not expanded to include generic params | |
let directImplRefs (td:TypeDefinition) = seq { | |
for fd in td.Fields do | |
yield fd.FieldType | |
for md in td.Methods do | |
yield! directMethodRefs md | |
} | |
/// Get a set of all types directly or indirectly referenced in the implementation of the methods of a TypeDefinition | |
let implRefs td = | |
td |> directImplRefs |> Seq.collect refWithGenericParams | |
/// Create a TypeWithDependencies from a TypeDefinition by gathering all its dependencies | |
let toTypeWithDependencies excludeCore td = | |
let isExternal = isExternalRef td excludeCore | |
let makeDep = toDependency isExternal | |
{ | |
typeDef = td |> typeName | |
isAuthored = td |> isAuthored | |
publicDeps = td |> publicRefs |> Seq.filter isNotNull |> Seq.map makeDep |> Set.ofSeq | |
implDeps = td |> implRefs |> Seq.filter isNotNull |> Seq.map makeDep |> Set.ofSeq | |
} | |
// recursively get all the types under a parent type | |
let rec withAllChildTypes (parent:TypeDefinition) = [ | |
yield parent | |
for child in parent.NestedTypes do | |
yield! child |> withAllChildTypes | |
] | |
/// Construct a TopLevelType from a top level TypeDefinition | |
let topLevelType excludeCore td = | |
// convert the typeDefs to Dependants | |
let nestedTypes = | |
td | |
|> withAllChildTypes | |
|> List.map (toTypeWithDependencies excludeCore) | |
|> Set.ofList | |
{ | |
name = td.FullName |> TltName | |
types = nestedTypes | |
} | |
/// Get all authored top level types from an assembly | |
let topLevelTypes excludeCore assemblyFileName = | |
Mono.Cecil.AssemblyDefinition.ReadAssembly(fileName=assemblyFileName).MainModule.Types | |
|> Seq.filter (isNotCoreType excludeCore) | |
|> Seq.filter isAuthored | |
|> Seq.map (topLevelType excludeCore) | |
/// Get the code size for an assembly. | |
/// Count instructions in non-core methods only. | |
let codeSize excludeCore assemblyFileName = | |
Mono.Cecil.AssemblyDefinition.ReadAssembly(fileName=assemblyFileName).MainModule.Types | |
|> Seq.filter (isNotCoreType excludeCore) | |
|> Seq.collect withAllChildTypes | |
|> Seq.collect (fun td-> td.Methods) | |
|> Seq.filter (fun md-> md.HasBody) | |
|> Seq.map (fun md-> md.Body.CodeSize) | |
|> Seq.sum | |
// ================================ | |
// Analyze dependencies between top level types | |
// ================================ | |
module TopLevelTypeDependencies = | |
/// Create a reverse lookup from type to top level type as a prerequisite for dependency analysis. | |
/// Each TypeName points to the TopLevelType it is contained in. | |
let createLookup tlts = | |
let childParentTuples tlt = | |
tlt.types | |
|> Seq.map (fun t -> t.typeDef,tlt.name) | |
tlts | |
|> Seq.collect childParentTuples | |
|> Map.ofSeq | |
/// Map the low-level types to corresponding top-level types | |
/// low-level types that are external are ignored | |
let mapDependency lookup = | |
function | |
| Internal lowLevelType -> | |
Map.tryFind lowLevelType lookup | |
| External lowLevelType -> | |
None | |
/// Map a top-level type to a DependencySet | |
let toDependencySet lookup getDeps tlt = | |
let allDeps = | |
tlt.types | |
|> Set.map getDeps // get each low-level type's dependencies | |
|> Set.unionMany // combine into one set, removing dups | |
|> Seq.map (mapDependency lookup) // lookup to get corresponding top level type (maybe) | |
|> Seq.choose id // convert from option to TltName | |
|> Set.ofSeq // remove dups again | |
|> Set.remove tlt.name // remove any self-reference | |
{dependent=tlt.name; dependencies=allDeps} | |
/// Convert a list of top level types to a list of DependencySets, using the public dependencies | |
let publicDependencies topLevelTypes = | |
let lookup = createLookup topLevelTypes | |
let getDeps t = t.publicDeps | |
let map = toDependencySet lookup getDeps | |
topLevelTypes |> Seq.map map | |
/// Convert a list of top level types to a list of DependencySets, using the all dependencies, including private and implementation | |
let allDependencies topLevelTypes = | |
let lookup = createLookup topLevelTypes | |
let getDeps t = Set.union t.publicDeps t.implDeps | |
let map = toDependencySet lookup getDeps | |
topLevelTypes |> Seq.map map | |
// ================================ | |
// Analyze the cyclic dependencies using QuickGraph | |
// ================================ | |
module CyclicDependencies = | |
open QuickGraph | |
open QuickGraph.Algorithms | |
open System.Collections.Generic | |
/// Convert a list of depSets into a graph | |
let toGraph depSets = | |
let createEdge source target = | |
new QuickGraph.SEdge<_>(source, target) | |
let edges (depSet:DependencySet) = | |
depSet.dependencies | |
|> Set.toSeq // must convert back to seq type | |
|> Seq.map (createEdge depSet.dependent) | |
let allEdges = depSets |> Seq.collect edges | |
QuickGraph.GraphExtensions.ToAdjacencyGraph(edges=allEdges) | |
/// get the strongly connected components of the dependencySets as a collection of TopLevelType sets | |
let scc depSets = | |
let graph = depSets |> toGraph | |
// The components are returned as a (type->index) dictionary, | |
// where all types in a component have the same index | |
let componentDict = new Dictionary<TltName,int>() :> IDictionary<_,_> | |
let componentDictRef = ref componentDict | |
let componentCount = graph.StronglyConnectedComponents(componentDictRef) | |
let components = | |
!componentDictRef | |
|> Seq.groupBy (fun kvp -> kvp.Value) // the index | |
|> Seq.map (fun (k,seq) -> | |
seq | |
|> Seq.map (fun kvp -> kvp.Key) // extract the type | |
|> Set.ofSeq // convert to a set | |
) | |
components | |
/// get the set of other types connected to T, or empty | |
let connectedTo components t = | |
let whereContainsT = Set.contains t | |
components | |
|> Seq.filter whereContainsT // by definition, only one component contains t | |
|> Seq.collect id // extract the data, if any | |
|> Set.ofSeq // convert back into a set | |
/// For each dependency set, remove all types that are not in the same | |
/// strongly connected component, leaving only the cyclic dependencies | |
/// Also remove trivial sets (size=1) | |
let toCyclicDependencies depSets = | |
// get all the components | |
let components = scc depSets | |
// remove non-connected | |
let intersectWithScc depSet = | |
let connected = connectedTo components depSet.dependent | |
let dependencies' = Set.intersect depSet.dependencies connected | |
{depSet with dependencies= dependencies'} | |
let depSets' = depSets |> Seq.map intersectWithScc | |
depSets' | |
// ================================ | |
// Generate the graph using GraphViz | |
// ================================ | |
module GraphViz = | |
// change this as needed for your local environment | |
let graphVizPath = @"C:\Program Files (x86)\Graphviz2.30\bin\" | |
let getName (TltName n) = | |
sprintf "\"%s\"" n // be sure to quote the type name! | |
let toCsv sep strList = | |
match strList with | |
| [] -> "" | |
| _ -> List.reduce (fun s1 s2 -> s1 + sep + s2) strList | |
let writeDepSet writer depSet = | |
let fromNode = getName depSet.dependent | |
let toNodes = | |
depSet.dependencies | |
|> Seq.map getName | |
|> Seq.sort // make it more human readable | |
|> Seq.toList | |
|> toCsv "; " | |
fprintfn writer " %s -> { rank=none; %s }" fromNode toNodes | |
// Create a DOT file for graphviz to read. | |
let createDotFile dotFilename depSets = | |
use writer = new System.IO.StreamWriter(path=dotFilename) | |
fprintfn writer "digraph G {" | |
fprintfn writer " page=\"40,60\"; " | |
fprintfn writer " ratio=auto;" | |
fprintfn writer " rankdir=LR;" | |
fprintfn writer " fontsize=10;" | |
depSets | |
|> Seq.filter (fun ds -> ds.dependencies.Count > 0) // ignore empty | |
|> Seq.sort // make it more human readable | |
|> Seq.iter (writeDepSet writer) | |
fprintfn writer " }" | |
// shell out to run a command line program | |
let startProcessAndCaptureOutput cmd cmdParams = | |
let debug = false | |
if debug then | |
printfn "Process: %s %s" cmd cmdParams | |
let si = new System.Diagnostics.ProcessStartInfo(cmd, cmdParams) | |
si.UseShellExecute <- false | |
si.RedirectStandardOutput <- true | |
use p = new System.Diagnostics.Process() | |
p.StartInfo <- si | |
if p.Start() then | |
if debug then | |
use stdOut = p.StandardOutput | |
stdOut.ReadToEnd() |> printfn "%s" | |
printfn "Process complete" | |
else | |
printfn "Process failed" | |
/// Generate an image file from a DOT file | |
/// algo = dot, neato | |
/// format = gif, png, jpg, svg | |
let generateImageFile dotFilename algo format imgFilename = | |
let cmd = sprintf @"""%s%s.exe""" graphVizPath algo | |
let inFile = System.IO.Path.Combine(__SOURCE_DIRECTORY__,dotFilename) | |
let outFile = System.IO.Path.Combine(__SOURCE_DIRECTORY__,imgFilename) | |
let cmdParams = sprintf "-T%s -o\"%s\" \"%s\"" format outFile inFile | |
startProcessAndCaptureOutput cmd cmdParams | |
// ================================ | |
// Various statistics | |
// ================================ | |
module Stats = | |
/// a record to hold some stats | |
type TypeStats = { | |
topLevelTypeCount:int // number of top level types | |
authoredTypeCount:int // number of types explicitly authored | |
allTypeCount: int // number of all internal types | |
} | |
let typeStats topLevelTypes = | |
let topLevelTypeCount = topLevelTypes |> Seq.length | |
let authoredTypeCount = | |
topLevelTypes | |
|> Seq.collect (fun tlt -> tlt.types ) | |
|> Seq.filter (fun td -> td.isAuthored) | |
|> Seq.length | |
let allTypeCount = | |
topLevelTypes | |
|> Seq.map (fun tlt -> Set.count tlt.types) | |
|> Seq.sum | |
{ topLevelTypeCount=topLevelTypeCount; authoredTypeCount=authoredTypeCount; allTypeCount=allTypeCount} | |
/// a record to hold some stats | |
type DependencyStats = { | |
depCount: int // number of dependency sets (same as number of top level types) | |
totalDepCount: int // sum of number of (internal) dependencies for all types | |
oneOrMoreDeps: int // number of top level types with one or more deps | |
threeOrMoreDeps: int // number of top level types with three or more deps | |
fiveOrMoreDeps: int // number of top level types with five or more deps | |
tenOrMoreDeps: int // number of top level types with ten or more deps | |
cycleCount:int // number of cycles | |
cycleParticipants: int // number of participants in cycles | |
maxComponentSize: int // max size of a strongly connected component | |
} | |
/// return the size of the largest component | |
let maxComponentSize components = | |
if Seq.isEmpty components | |
then 0 // max won't work on an empty set | |
else | |
components | |
|> Seq.map (fun set -> Set.count set) | |
|> Seq.max | |
let dependencyStats depSets = | |
let depCount = depSets |> Seq.length | |
let totalDepCount = depSets |> Seq.sumBy (fun depSet -> depSet.dependencies.Count) | |
let getCount max = | |
depSets | |
|> Seq.map (fun depSet -> depSet.dependencies.Count) | |
|> Seq.filter (fun count -> count >= max) | |
|> Seq.length | |
let oneOrMoreDeps = getCount 1 | |
let threeOrMoreDeps = getCount 3 | |
let fiveOrMoreDeps = getCount 5 | |
let tenOrMoreDeps = getCount 10 | |
let nonTrivialComponents = CyclicDependencies.scc depSets |> Seq.filter (fun set -> set.Count > 1) | |
let cycleCount = nonTrivialComponents |> Seq.length | |
let maxComponentSize = maxComponentSize nonTrivialComponents | |
let cyclicDeps = CyclicDependencies.toCyclicDependencies depSets |> Seq.filter (fun ds -> ds.dependencies.Count > 0) // ignore empty | |
let cycleParticipants = cyclicDeps |> Seq.length | |
{ depCount=depCount; | |
totalDepCount=totalDepCount; oneOrMoreDeps=oneOrMoreDeps; | |
threeOrMoreDeps=threeOrMoreDeps; fiveOrMoreDeps=fiveOrMoreDeps; tenOrMoreDeps=tenOrMoreDeps; | |
cycleCount=cycleCount; cycleParticipants=cycleParticipants; maxComponentSize=maxComponentSize } | |
/// a record to hold some stats | |
type FrequencyStats = { | |
value:int | |
count:int | |
} | |
let typeFrequency topLevelTypes = | |
let authCount tds = | |
tds | |
|> Seq.filter (fun td -> td.isAuthored) | |
|> Seq.length | |
let authoredTypeFrequencies = | |
topLevelTypes | |
|> Seq.groupBy (fun tlt -> authCount tlt.types) | |
|> Seq.map (fun (k,v) -> {value=k;count=Seq.length v} ) | |
let allTypeFrequencies = | |
topLevelTypes | |
|> Seq.groupBy (fun tlt -> Set.count tlt.types) | |
|> Seq.map (fun (k,v) -> {value=k;count=Seq.length v} ) | |
authoredTypeFrequencies,allTypeFrequencies | |
let depFrequency depSets = | |
depSets | |
|> Seq.groupBy (fun depSet -> Set.count depSet.dependencies) | |
|> Seq.map (fun (k,v) -> {value=k;count=Seq.length v} ) | |
// ================================ | |
// For REPL or debugging | |
// ================================ | |
module Output = | |
open Stats | |
let printDependencies depSets = | |
let printDependencySet depSet = | |
depSet.dependent |> printfn "%A" | |
depSet.dependencies |> Seq.toList |> List.sort |> List.iter (printfn "\t%A") | |
depSets | |
|> Seq.sortBy (fun depSet -> depSet.dependent) | |
|> Seq.iter printDependencySet | |
let printStats projectName codeSize typeStats publicDepStats allDepStats = | |
let codeToTopTypeRatio = codeSize / typeStats.topLevelTypeCount | |
let codeToAuthoredTypeRatio = codeSize / typeStats.authoredTypeCount | |
let codeToAllTypeRatio = codeSize / typeStats.allTypeCount | |
let topToAllRatio = float typeStats.topLevelTypeCount / float typeStats.allTypeCount | |
let topToAuthoredRatio = float typeStats.topLevelTypeCount / float typeStats.authoredTypeCount | |
printfn "===========================================" | |
printfn "Stats for %s" projectName | |
printfn "===========================================" | |
printfn " - CodeSize=%i TopLevelTypes=%i AllTypes=%i" codeSize typeStats.topLevelTypeCount typeStats.allTypeCount | |
printfn " - Instructions per top level type=%i (Instructions per authored type=%i)" codeToTopTypeRatio codeToAuthoredTypeRatio | |
printfn " - Ratio of top level types to authored types = %.2f " topToAuthoredRatio | |
printfn " - Ratio of top level types to all types = %.2f " topToAllRatio | |
let avgDepCount stats = float stats.totalDepCount / float typeStats.topLevelTypeCount | |
let ( <%> )top bottom = float top * 100.0 / float bottom | |
let pcOneOrMoreDeps stats = stats.oneOrMoreDeps <%> typeStats.topLevelTypeCount | |
let pcThreeOrMoreDeps stats = stats.threeOrMoreDeps <%> typeStats.topLevelTypeCount | |
let pcFiveOrMoreDeps stats = stats.fiveOrMoreDeps <%> typeStats.topLevelTypeCount | |
let pcTenOrMoreDeps stats = stats.tenOrMoreDeps <%> typeStats.topLevelTypeCount | |
printfn "Analysis of implementation dependencies for %s" projectName | |
printfn " - Avg # of dependencies=%.2f One or more=%.1f%%, 3 or more=%.1f%%, 5 or more=%.1f%%, 10 or more=%.1f%%" (avgDepCount allDepStats) (pcOneOrMoreDeps allDepStats) (pcThreeOrMoreDeps allDepStats) (pcFiveOrMoreDeps allDepStats) (pcTenOrMoreDeps allDepStats) | |
printfn " - Cycle count=%i cycleParticipants=%i maxComponentSize=%i" allDepStats.cycleCount allDepStats.cycleParticipants allDepStats.maxComponentSize | |
printfn "Analysis of public dependencies for %s" projectName | |
printfn " - Avg # of dependencies=%.2f One or more=%.1f%%, 3 or more=%.1f%%, 5 or more=%.1f%%, 10 or more=%.1f%%" (avgDepCount publicDepStats) (pcOneOrMoreDeps publicDepStats) (pcThreeOrMoreDeps publicDepStats) (pcFiveOrMoreDeps publicDepStats) (pcTenOrMoreDeps publicDepStats) | |
printfn " - Cycle count=%i cycleParticipants=%i maxComponentSize=%i" publicDepStats.cycleCount publicDepStats.cycleParticipants publicDepStats.maxComponentSize | |
let writeFrequenciesToFile projectName extension frequencies = | |
let filename = projectName + extension | |
use writer = new System.IO.StreamWriter(path=filename) | |
fprintfn writer "Project,Value,Count" | |
let writeFreq freq = | |
fprintfn writer "%s,%i,%i" projectName freq.value freq.count | |
frequencies | |
|> Seq.sortBy (fun f -> f.value) | |
|> Seq.iter writeFreq | |
// ================================ | |
// Main functions | |
// ================================ | |
/// Analysis should not include the F# core types except | |
/// for the projects explicitly listed | |
let ignoreCoreTypes projectName = | |
projectName <> "fsCore" && projectName <> "fsPowerPack" && projectName <> "fsharpCompiler" | |
let analyzeAndGenerate projectName assemblyName = | |
let excludeCore = ignoreCoreTypes projectName | |
// analyze | |
let topLevelTypes = AssemblyTypes.topLevelTypes excludeCore assemblyName | |
let publicDeps = TopLevelTypeDependencies.publicDependencies topLevelTypes | |
let publicCycles = CyclicDependencies.toCyclicDependencies publicDeps | |
let allDeps = TopLevelTypeDependencies.allDependencies topLevelTypes | |
let allCycles = CyclicDependencies.toCyclicDependencies allDeps | |
// stats | |
let codeSize = AssemblyTypes.codeSize excludeCore assemblyName | |
let typeStats = Stats.typeStats topLevelTypes | |
let publicDepStats = Stats.dependencyStats publicDeps | |
let allDepStats = Stats.dependencyStats allDeps | |
do Output.printStats projectName codeSize typeStats publicDepStats allDepStats | |
let generateFile depSet extension = | |
// create DOT file | |
let dotFilename = projectName + extension | |
GraphViz.createDotFile dotFilename depSet | |
// create SVG file | |
let svgFilename = dotFilename + ".svg" | |
GraphViz.generateImageFile dotFilename "dot" "svg" svgFilename | |
do generateFile publicDeps ".public.dot" | |
do generateFile publicCycles ".public.cycles.dot" | |
do generateFile allDeps ".all.dot" | |
do generateFile allCycles ".all.cycles.dot" | |
let tabularStats projects = | |
printfn "Project,CodeSize,TopLevelTypes,AuthoredTypes,AllTypes,AllTotalDepCount,AllOneOrMoreDepCount,AllThreeOrMoreDepCount,AllFiveOrMoreDepCount,AllTenOrMoreDepCount,AllMaxComponentSize,AllCycleCount,AllCycleParticipants,PubTotalDepCount,PubOneOrMoreDepCount,PubThreeOrMoreDepCount,PubFiveOrMoreDepCount,PubTenOrMoreDepCount,PubMaxComponentSize,PubCycleCount,PubCycleParticipants" | |
for (projectName,assemblyName) in projects do | |
let excludeCore = ignoreCoreTypes projectName | |
// analyze | |
let topLevelTypes = AssemblyTypes.topLevelTypes excludeCore assemblyName | |
let publicDeps = TopLevelTypeDependencies.publicDependencies topLevelTypes | |
let publicCycles = CyclicDependencies.toCyclicDependencies publicDeps | |
let allDeps = TopLevelTypeDependencies.allDependencies topLevelTypes | |
let allCycles = CyclicDependencies.toCyclicDependencies allDeps | |
// stats | |
let codeSize = AssemblyTypes.codeSize excludeCore assemblyName | |
let typeStats = Stats.typeStats topLevelTypes | |
let publicDepStats = Stats.dependencyStats publicDeps | |
let allDepStats = Stats.dependencyStats allDeps | |
printfn "%s,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i" projectName codeSize typeStats.topLevelTypeCount typeStats.authoredTypeCount typeStats.allTypeCount allDepStats.totalDepCount allDepStats.oneOrMoreDeps allDepStats.threeOrMoreDeps allDepStats.fiveOrMoreDeps allDepStats.tenOrMoreDeps allDepStats.maxComponentSize allDepStats.cycleCount allDepStats.cycleParticipants publicDepStats.totalDepCount publicDepStats.oneOrMoreDeps publicDepStats.threeOrMoreDeps publicDepStats.fiveOrMoreDeps publicDepStats.tenOrMoreDeps publicDepStats.maxComponentSize publicDepStats.cycleCount publicDepStats.cycleParticipants | |
let frequenciesStats projects = | |
for (projectName,assemblyName) in projects do | |
let excludeCore = ignoreCoreTypes projectName | |
// analyze | |
let topLevelTypes = AssemblyTypes.topLevelTypes excludeCore assemblyName | |
let allDeps = TopLevelTypeDependencies.allDependencies topLevelTypes | |
// stats | |
let authored,all = Stats.typeFrequency topLevelTypes | |
let deps = Stats.depFrequency allDeps | |
Output.writeFrequenciesToFile projectName ".authored.freq.csv" authored | |
Output.writeFrequenciesToFile projectName ".types.freq.csv" all | |
Output.writeFrequenciesToFile projectName ".deps.freq.csv" deps | |
// =============================================== | |
// C# Projects | |
// =============================================== | |
// C# Project - Roslyn code | |
let roslyn = @"packages\Roslyn\Microsoft.CodeAnalysis.dll" | |
let roslynCsharp = @"packages\Roslyn\Microsoft.CodeAnalysis.CSharp.dll" | |
// =============================================== | |
// F# Projects | |
// =============================================== | |
// F# Project - FSharp.Compiler | |
let fsharpCompiler = @"packages\FSharp.Compiler\FSharp.Compiler.dll" | |
let csProjects = [ | |
("roslyn",roslyn) | |
("roslynCsharp",roslynCsharp) | |
] | |
let fsProjects = [ | |
("fsharpCompiler",fsharpCompiler) | |
] | |
// =============================================== | |
// Generate SVG files | |
// =============================================== | |
analyzeAndGenerate "roslyn" roslyn | |
analyzeAndGenerate "roslynCsharp" roslynCsharp | |
analyzeAndGenerate "fsharpCompiler" fsharpCompiler | |
// =============================================== | |
// All stats tabulated | |
// =============================================== | |
tabularStats csProjects | |
tabularStats fsProjects | |
frequenciesStats csProjects | |
frequenciesStats fsProjects |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment