Skip to content

Instantly share code, notes, and snippets.

@swlaschin
Last active December 27, 2015 21:48
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save swlaschin/10558434 to your computer and use it in GitHub Desktop.
F# scripts for analysis of Roslyn vs F# compiler
(*
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 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