Skip to content

Instantly share code, notes, and snippets.

@j-ulrich
Last active August 30, 2019 08:12
Show Gist options
  • Save j-ulrich/35f345007809c77403a8ee88f4d9db11 to your computer and use it in GitHub Desktop.
Save j-ulrich/35f345007809c77403a8ee88f4d9db11 to your computer and use it in GitHub Desktop.
Testing performance of Qbs resolve
#!/bin/bash
PROJECTS_BASE_DIR=${1:-"projects"}
function generateQbsProject
{
python3 generateQbsProject.py $1 $2 "${PROJECTS_BASE_DIR}/$1-$2-direct" --depends-mode direct
python3 generateQbsProject.py $1 $2 "${PROJECTS_BASE_DIR}/$1-$2-implicit" --depends-mode implicit
python3 generateQbsProject.py $1 $2 "${PROJECTS_BASE_DIR}/$1-$2-explicit" --depends-mode explicit
python3 generateQbsProject.py $1 $2 "${PROJECTS_BASE_DIR}/withLeafExports/$1-$2-direct" --depends-mode direct --leaf-exports cpp Qt.core
python3 generateQbsProject.py $1 $2 "${PROJECTS_BASE_DIR}/withLeafExports/$1-$2-implicit" --depends-mode implicit --leaf-exports cpp Qt.core
python3 generateQbsProject.py $1 $2 "${PROJECTS_BASE_DIR}/withLeafExports/$1-$2-explicit" --depends-mode explicit --leaf-exports cpp Qt.core
python3 generateQbsProject.py $1 $2 "${PROJECTS_BASE_DIR}/withAdditionalDepends/$1-$2-direct" --depends-mode direct --additional-depends cpp Qt.core
python3 generateQbsProject.py $1 $2 "${PROJECTS_BASE_DIR}/withAdditionalDepends/$1-$2-implicit" --depends-mode implicit --additional-depends cpp Qt.core
python3 generateQbsProject.py $1 $2 "${PROJECTS_BASE_DIR}/withAdditionalDepends/$1-$2-explicit" --depends-mode explicit --additional-depends cpp Qt.core
}
generateQbsProject 4 8
generateQbsProject 9 2
#!python3
import argparse
import copy
import os
from textwrap import dedent
def main():
args = parseCommandline()
os.makedirs( args.target_dir, exist_ok=True )
generator = Generator( args )
generator.generateProject()
def parseCommandline():
argParser = argparse.ArgumentParser( description='''Generates a Qbs dummy project with a specified size of the build graph''' )
argParser.add_argument( '--depends-mode', choices=[ 'direct', 'explicit', 'implicit' ], default='direct' )
argParser.add_argument( '--leaf-exports', nargs='+', action='append' )
argParser.add_argument( '--additional-depends', nargs='+', action='append' )
argParser.add_argument( 'depth', type=int )
argParser.add_argument( 'deps_per_node', type=int )
argParser.add_argument( 'target_dir' )
return argParser.parse_args()
class Generator:
def __init__( self, args ):
self.depth = args.depth
self.deps_per_node = args.deps_per_node
self.target_dir = args.target_dir
self.depends_mode = args.depends_mode
self.leaf_exports = args.leaf_exports
self.additional_depends = args.additional_depends
def generateProject( self ):
self.generateRootNode()
if self.depth > 1:
self.generateTree( self.depth - 1, [] )
def generateRootNode( self ):
dependsList = self.generateNodeDependsList( [] ) if self.depth > 1 else ''
dependsList += '\n' + self.generateAdditionalDependsList()
dependsList = dependsList.strip( ' \n' )
fileList = self.generateNodeFileList()
code = Generator.rootTemplate.format( dependsList=dependsList, fileList=fileList )
self.writeNode( 'root', code )
def generateAdditionalDependsList( self ):
return Generator._generateDependsList( self.additional_depends )
@staticmethod
def _generateDependsList( depends, indent='\t' ):
if not depends:
return ''
dependsList = []
for dependNames in depends:
for dependName in dependNames:
dependsList.append( indent + dependsStatement( dependName ) )
return '\n'.join( dependsList )
def generateNodeDependsList( self, path, maxDepth = None ):
dependsList = []
for dependId in range( self.deps_per_node ):
dependPath = extendedPath( path, dependId )
dependsName = pathToName( dependPath )
dependsList.append( '\t' + dependsStatement( dependsName ) )
if self.depends_mode == 'explicit':
pathLength = len( path )
depthLimit = self.depth - 2 if maxDepth is None else maxDepth
if pathLength < depthLimit:
dependsList.append( self.generateNodeDependsList( dependPath, maxDepth ) ) # RECURSION!
return '\n'.join( dependsList )
def generateNodeFileList( self ):
files = []
idPath = []
for depth in range( self.depth - 1 ):
while True:
while len( idPath ) < depth + 1:
idPath.append( 0 )
for i in range( self.deps_per_node ):
files.append( pathToName( idPath ) )
idPath[ -1 ] += 1
while ( idPath and idPath[ -1 ] >= self.deps_per_node - 1 ):
idPath.pop()
if idPath:
idPath[ -1 ] += 1
else:
break
return '\n'.join( [ '\t"{}.qbs",'.format( file ) for file in files ] ) or ''
def generateTree( self, depth, parentPath ):
if depth == 1:
for id in range( self.deps_per_node ):
self.generateLeafNode( extendedPath( parentPath, id ) )
else:
for id in range( self.deps_per_node ):
currentPath = extendedPath( parentPath, id )
self.generateInterNode( currentPath )
self.generateTree( depth - 1, currentPath ) # RECURSION!
def generateLeafNode( self, path ):
dependsList = self.generateAdditionalDependsList()
leafExportList = self.generateLeafExportList()
code = Generator.leafTemplate.format( dependsList=dependsList, leafExportList=leafExportList )
self.writeNode( pathToName( path ), code )
def generateLeafExportList( self ):
return Generator._generateDependsList( self.leaf_exports, '\t\t' )
def generateInterNode( self, path ):
dependsList = self.generateNodeDependsList( path )
dependsList += '\n' + self.generateAdditionalDependsList()
dependsList = dependsList.strip( ' \n' )
interNodeExportList = self.generateInterNodeExportList( path )
code = Generator.interNodeTemplate.format( dependsList=dependsList, interNodeExportList=interNodeExportList )
self.writeNode( pathToName( path ), code )
def generateInterNodeExportList( self, path ):
if self.depends_mode == 'implicit':
return '\n'.join( [ '\t' + x for x in self.generateNodeDependsList( path, len( path ) + 1 ).split( '\n' ) ] )
return ''
def writeNode( self, name, code ):
with open( os.path.join( self.target_dir, name + '.qbs' ), 'w', encoding='utf-8' ) as file:
file.write( code )
leafTemplate=dedent('''
import qbs
Product {{
{dependsList}
Export {{
{leafExportList}
}}
}}
''')
interNodeTemplate=dedent('''
import qbs
Product {{
{dependsList}
Export {{
{interNodeExportList}
}}
}}
''')
rootTemplate=dedent('''
import qbs
Project {{
references: [
{fileList}
]
Product {{
{dependsList}
}}
}}
''')
def extendedPath( path, id ):
return path + [ str( id ) ]
def pathToName( path ):
return '-'.join( [ str( entry ) for entry in path ] )
def dependsStatement( dependsName ):
return f'Depends {{ name: "{dependsName}" }}'
if __name__ == '__main__':
main()

Test Setup

Qbs 1.12.2 on macOS 10.14.5
CPU: 2,9 GHz Intel Core i7, 8 logical cores
RAM: 16 GB 2133 MHz LPDDR3
SSD drive

Project Variants

These results were created using different variants of projects. The projects differ in the following aspects.

Dimensions

The projects differ in the tree depth and the number of direct children per node in the build graph:

  • "4-8": tree levels: 4; 8 direct children per node => 585 Products
  • "9-2": tree levels: 9; 2 direct children per node => 511 Products

Dependency Modes

The projects differ in the way the nodes depend on their children:

  • "direct:" nodes depend on their direct children but export nothing
  • "implicit": nodes depend on their direct children and export these dependencies
  • "explicit": nodes explicitly depend on all nodes of the their subtree and export nothing

External Dependencies

The projects differ in where dependencies to external modules are included:

  • nothing: no external dependencies
  • "withAdditionalDepends": each node explicitly depends on "cpp" and "Qt.core"
  • "withLeafExports": leaf nodes export "cpp" and "Qt.core"

Note: Only the variants with

  • "withAdditionalDepends"
  • "withLeafExports" + "implicit" dependency mode

ensure that all nodes depend on "cpp" and "Qt.core".

Results

Project Initial Resolve Time Re-Resolve Time Build Graph Size No. of Depends Items
4-8-direct 2.808s 2.538s 1.0M 584
4-8-implicit 3.558s 3.469s 1.2M 1160
4-8-explicit 3.417s 3.273s 1.2M 1672
9-2-direct 2.343s 2.201s 1.0M 510
9-2-implicit 5.579s 5.579s 1.4M 1018
9-2-explicit 3.631s 3.623s 1.4M 3586
withAdditionalDepends/4-8-direct 29.257s 30.705s 12.0M 1754
withAdditionalDepends/4-8-implicit 30.649s 30.526s 12.0M 2330
withAdditionalDepends/4-8-explicit 30.103s 31.725s 12.0M 2842
withAdditionalDepends/9-2-direct 25.691s 31.440s 11.0M 1532
withAdditionalDepends/9-2-implicit 29.684s 32.190s 11.0M 2040
withAdditionalDepends/9-2-explicit 27.105s 27.664s 11.0M 4608
withLeafExports/4-8-direct 25.598s 25.673s 2.8M 1608
withLeafExports/4-8-implicit 36.688s 34.438s 3.1M 2184
withLeafExports/4-8-explicit 29.080s 30.262s 3.1M 2696
withLeafExports/9-2-direct 18.374s 17.615s 3.7M 1022
withLeafExports/9-2-implicit 50.530s 50.010s 6.5M 1530
withLeafExports/9-2-explicit 29.111s 29.514s 6.4M 4098

Re-Resolve: just touched the "root.qbs" and resolved again

Summary

The worst resolve time was observed when using transitive dependencies (Export items) with a deep tree. On the other hand, transitive dependencies save writing redundant Depends items.

Explicitly depending on needed modules increases the build graph size and the number of Depends items to be written, but is faster when resolving.

ModuleLoader Timing

Project: withLeafExports/9-2-implicit

Initial Resolve

Starting activity 'ModuleLoader'.
	Project file loading and parsing took 134ms.
	Preparing products took 1ms.
	Setting up product dependencies took 8s, 756ms.
		Setting up transitive product dependencies took 845ms.
	Handling products took 33s, 333ms.
		Running Probes took 18s, 67ms.
		5355 probes encountered, 5 configure scripts executed, 5348 re-used from current run, 0 re-used from earlier run.
	Property checking took 412ms.
Activity 'ModuleLoader' took 44s, 99ms.

Re-Resolve

Starting activity 'ModuleLoader'.
	Project file loading and parsing took 123ms.
	Preparing products took 0ms.
	Setting up product dependencies took 10s, 76ms.
		Setting up transitive product dependencies took 837ms.
	Handling products took 32s, 798ms.
		Running Probes took 17s, 543ms.
		5355 probes encountered, 0 configure scripts executed, 0 re-used from current run, 5355 re-used from earlier run.
	Property checking took 409ms.
Activity 'ModuleLoader' took 44s, 630ms.

Project: withAdditionalDepends/9-2-explicit

Starting activity 'ModuleLoader'.
	Project file loading and parsing took 214ms.
	Preparing products took 0ms.
	Setting up product dependencies took 1s, 644ms.
		Setting up transitive product dependencies took 14ms.
	Handling products took 14s, 267ms.
		Running Probes took 11s, 736ms.
		3577 probes encountered, 5 configure scripts executed, 3570 re-used from current run, 0 re-used from earlier run.
	Property checking took 164ms.
Activity 'ModuleLoader' took 17s, 910ms.
#!/bin/bash
QBS_PROFILE=${1:?"Qbs profile is required"}
PROJECTS_BASE_DIR=${2:-"projects"}
BUILD_BASE_DIR=${3:-"build"}
for projectRoot in $(find "$PROJECTS_BASE_DIR" -name root.qbs -type f)
do
projectDir=$(dirname "$projectRoot")
projectName=${projectDir#"$PROJECTS_BASE_DIR/"}
echo "> Resolving $projectName"
time qbs resolve profile:"${PROFILE}" -d "$BUILD_BASE_DIR/$projectName" -f "$projectRoot"
echo "Build graph size: $( du -h "$BUILD_BASE_DIR/$projectName/default/default.bg" | cut -f1 )"
dependsCount=0
for file in $(find "${projectDir}" -name "*.qbs" -type f)
do
fileCount=$(grep "Depends" "$file" --count)
dependsCount=$(( $dependsCount + $fileCount ))
done
echo "Depends statemenets in code: $dependsCount"
echo
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment