Skip to content

Commit 1a702ef

Browse files
authored
Implement multiple performance enhancements #70 (#110)
* Run celery worker with the "threads" pool implementation Signed-off-by: Thomas Druez <tdruez@nexb.com> * Implement parallelization with ProcessPoolExecutor for file and package scans #70 Signed-off-by: Thomas Druez <tdruez@nexb.com> * Consistent queryset calls using project.relation in place of Relation.objects #70 Signed-off-by: Thomas Druez <tdruez@nexb.com> * Add settings configuration for the MAX_WORKERS #70 Signed-off-by: Thomas Druez <tdruez@nexb.com> * Add a SCANCODE_PROCESSES settings to control the multiprocessing CPUs #70 Signed-off-by: Thomas Druez <tdruez@nexb.com> * Optimize "tag" type pipes using the update() API #70 in place of save() on the QuerySet iteration Signed-off-by: Thomas Druez <tdruez@nexb.com> * Add entries in the CHANGELOG #70 Signed-off-by: Thomas Druez <tdruez@nexb.com> * Add size=0 as a lookup in the .empty() queryset method #70 Signed-off-by: Thomas Druez <tdruez@nexb.com> * Refactor make_codebase_resource to remove the extra get query #70 Signed-off-by: Thomas Druez <tdruez@nexb.com> * Reduce the pagination by 10 in the ProjectListView #70 Signed-off-by: Thomas Druez <tdruez@nexb.com>
1 parent 0086fee commit 1a702ef

16 files changed

Lines changed: 225 additions & 111 deletions

File tree

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33

44
### v1.1.1 (unreleased)
55

6+
- Run celery worker with the "threads" pool implementation.
7+
Implement parallelization with ProcessPoolExecutor for file and package scans.
8+
Add a SCANCODE_PROCESSES settings to control the multiprocessing CPUs count.
9+
https://github.com/nexB/scancode.io/issues/70
10+
11+
- Optimize "tag" type pipes using the update() API in place of save() on the QuerySet
12+
iteration.
13+
https://github.com/nexB/scancode.io/issues/70
14+
615
- Use the extractcode API for the Docker pipeline.
716
This change helps with performance and results consistency between pipelines.
817
https://github.com/nexB/scancode.io/issues/70

docker-compose.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ services:
1515
build: .
1616
command: celery --app scancodeio worker
1717
--loglevel=INFO
18-
--soft-time-limit=21600 --time-limit=22000
18+
--concurrency 1 --pool threads
1919
--events -Ofair --prefetch-multiplier=1
20+
--soft-time-limit=21600 --time-limit=22000
2021
env_file:
2122
- docker.env
2223
volumes:
@@ -32,7 +33,7 @@ services:
3233
command: sh -c "
3334
./manage.py migrate &&
3435
./manage.py collectstatic --no-input &&
35-
gunicorn scancodeio.wsgi:application --bind :8000 --timeout 600 --workers 3"
36+
gunicorn scancodeio.wsgi:application --bind :8000 --timeout 600 --workers 2"
3637
env_file:
3738
- docker.env
3839
expose:

etc/nginx/conf.d/default.conf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ server {
1010
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
1111
proxy_set_header Host $host;
1212
proxy_redirect off;
13-
client_max_body_size 100M;
14-
proxy_read_timeout 600s;
13+
client_max_body_size 10G;
14+
proxy_read_timeout 600s;
1515
}
1616

1717
location /static/ {

scancodeio/settings/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@
4545

4646
SCANCODE_DEFAULT_OPTIONS = env.list("SCANCODE_DEFAULT_OPTIONS", default=[])
4747

48+
# Set the number of parallel processes to use for ScanCode related scan execution.
49+
# If the SCANCODE_PROCESSES argument is not set, defaults to the number of CPUs minus 1.
50+
SCANCODE_PROCESSES = env.int("SCANCODE_PROCESSES", default=None)
51+
4852
# Application definition
4953

5054
INSTALLED_APPS = (

scanpipe/api/serializers.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,8 @@
2121
# Visit https://github.com/nexB/scancode.io for support and download.
2222

2323
from django.apps import apps
24-
from django.utils.functional import lazy
2524

2625
from rest_framework import serializers
27-
from rest_framework.exceptions import ValidationError
2826

2927
from scanpipe.api import ExcludeFromListViewMixin
3028
from scanpipe.models import CodebaseResource

scanpipe/api/views.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# Visit https://github.com/nexB/scancode.io for support and download.
2222

2323
from django.apps import apps
24+
from django.core.exceptions import ObjectDoesNotExist
2425
from django.db import transaction
2526

2627
from rest_framework import mixins
@@ -36,10 +37,7 @@
3637
from scanpipe.api.serializers import ProjectErrorSerializer
3738
from scanpipe.api.serializers import ProjectSerializer
3839
from scanpipe.api.serializers import RunSerializer
39-
from scanpipe.models import CodebaseResource
40-
from scanpipe.models import DiscoveredPackage
4140
from scanpipe.models import Project
42-
from scanpipe.models import ProjectError
4341
from scanpipe.models import Run
4442
from scanpipe.views import project_results_json_response
4543

@@ -97,9 +95,7 @@ def pipelines(self, request, *args, **kwargs):
9795
@action(detail=True)
9896
def resources(self, request, *args, **kwargs):
9997
project = self.get_object()
100-
queryset = CodebaseResource.objects.project(project).prefetch_related(
101-
"discovered_packages"
102-
)
98+
queryset = project.codebaseresources.prefetch_related("discovered_packages")
10399

104100
paginated_qs = self.paginate_queryset(queryset)
105101
serializer = CodebaseResourceSerializer(paginated_qs, many=True)
@@ -109,7 +105,7 @@ def resources(self, request, *args, **kwargs):
109105
@action(detail=True)
110106
def packages(self, request, *args, **kwargs):
111107
project = self.get_object()
112-
queryset = DiscoveredPackage.objects.project(project)
108+
queryset = project.discoveredpackages.all()
113109

114110
paginated_qs = self.paginate_queryset(queryset)
115111
serializer = DiscoveredPackageSerializer(paginated_qs, many=True)
@@ -119,7 +115,7 @@ def packages(self, request, *args, **kwargs):
119115
@action(detail=True)
120116
def errors(self, request, *args, **kwargs):
121117
project = self.get_object()
122-
queryset = ProjectError.objects.project(project)
118+
queryset = project.projecterrors.all()
123119

124120
paginated_qs = self.paginate_queryset(queryset)
125121
serializer = ProjectErrorSerializer(paginated_qs, many=True)
@@ -130,11 +126,11 @@ def errors(self, request, *args, **kwargs):
130126
def file_content(self, request, *args, **kwargs):
131127
project = self.get_object()
132128
path = request.query_params.get("path")
133-
codebase_resources = CodebaseResource.objects.project(project)
129+
codebase_resources = project.codebaseresources.all()
134130

135131
try:
136132
codebase_resource = codebase_resources.get(path=path)
137-
except CodebaseResource.DoesNotExist:
133+
except ObjectDoesNotExist:
138134
message = {"status": "Resource not found. Use ?path=<resource_path>"}
139135
return Response(message, status=status.HTTP_400_BAD_REQUEST)
140136

scanpipe/models.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,13 +453,16 @@ class SaveProjectErrorMixin:
453453
Use `SaveProjectErrorMixin` on a model to create a ProjectError entry
454454
from a raised exception during `save()` in place of stopping the analysis
455455
process.
456+
The creation of ProjectError can be skipped providing False for the `save_error`
457+
argument.
456458
"""
457459

458-
def save(self, *args, **kwargs):
460+
def save(self, *args, save_error=True, **kwargs):
459461
try:
460462
super().save(*args, **kwargs)
461463
except Exception as error:
462-
self.add_error(error)
464+
if save_error:
465+
self.add_error(error)
463466

464467
@classmethod
465468
def check(cls, **kwargs):
@@ -611,6 +614,9 @@ def status(self, status=None):
611614
def no_status(self):
612615
return self.filter(status="")
613616

617+
def empty(self):
618+
return self.filter(Q(size__isnull=True) | Q(size=0))
619+
614620
def in_package(self):
615621
return self.filter(discovered_packages__isnull=False)
616622

scanpipe/pipes/__init__.py

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,35 +33,37 @@
3333

3434
def make_codebase_resource(project, location, rootfs_path=None):
3535
"""
36-
Get or create and return a CodebaseResource with `location` absolute path
37-
for the `project` Project.
36+
Create a CodebaseResource with the `location` absolute path for the `project`.
3837
3938
The `location` of this Resource must be rooted in `project.codebase_path`.
4039
4140
`rootfs_path` is an optional path relative to a rootfs root within an
4241
Image/VM filesystem context. e.g.: "/var/log/file.log"
4342
4443
All paths use the POSIX separators.
44+
45+
If a CodebaseResource already exists in the `project` for with the same path,
46+
the error raised on save() is not stored in the database and the creation is
47+
skipped.
4548
"""
46-
location = location.rstrip("/")
47-
codebase_path = str(project.codebase_path)
48-
assert location.startswith(
49-
codebase_path
50-
), f"Location: {location} is not under project/codebase: {codebase_path}"
49+
resource_location = location.rstrip("/")
50+
codebase_dir = str(project.codebase_path)
51+
52+
assert resource_location.startswith(
53+
codebase_dir
54+
), f"Location: {resource_location} is not under project/codebase/: {codebase_dir}"
5155

52-
path = location.replace(codebase_path, "")
56+
resource_data = scancode.get_resource_info(location=resource_location)
5357

54-
resource_defaults = {}
5558
if rootfs_path:
56-
resource_defaults["rootfs_path"] = rootfs_path
57-
resource_defaults.update(scancode.get_resource_info(location=location))
59+
resource_data["rootfs_path"] = rootfs_path
5860

59-
codebase_resource, _created = CodebaseResource.objects.get_or_create(
61+
codebase_resource = CodebaseResource(
6062
project=project,
61-
path=path,
62-
defaults=resource_defaults,
63+
path=resource_location.replace(codebase_dir, ""),
64+
**resource_data,
6365
)
64-
return codebase_resource
66+
codebase_resource.save(save_error=False)
6567

6668

6769
def update_or_create_package(project, package_data):
@@ -137,22 +139,21 @@ def analyze_scanned_files(project):
137139
"""
138140
Set the status for CodebaseResource with unknown or no licenses.
139141
"""
140-
queryset = CodebaseResource.objects.project(project).files().status("scanned")
141-
queryset.has_no_licenses().update(status="no-licenses")
142+
scanned_files = project.codebaseresources.files().status("scanned")
143+
144+
scanned_files.has_no_licenses().update(status="no-licenses")
142145

143-
for codebase_resource in queryset:
146+
for codebase_resource in scanned_files:
144147
if has_unknown_license(codebase_resource):
145148
codebase_resource.status = "unknown-license"
146149
codebase_resource.save()
147150

148151

149152
def tag_not_analyzed_codebase_resources(project):
150153
"""
151-
Flag as "not-analyzed" the `CodebaseResource` without a status of the
152-
provided `project`
154+
Flag as "not-analyzed" the `CodebaseResource` without a status of the `project`.
153155
"""
154-
no_status = CodebaseResource.objects.project(project).no_status()
155-
no_status.update(status="not-analyzed")
156+
project.codebaseresources.no_status().update(status="not-analyzed")
156157

157158

158159
def normalize_path(path):

scanpipe/pipes/compliance.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
2121
# Visit https://github.com/nexB/scancode.io for support and download.
2222

23-
from scanpipe.models import CodebaseResource
2423
from scanpipe.pipes import scancode
2524

2625
"""
@@ -56,7 +55,7 @@ def analyze_compliance_licenses(project):
5655
"""
5756
Scan compliance licenses status for the provided `project`.
5857
"""
59-
qs = CodebaseResource.objects.project(project).status("compliance-licenses")
58+
qs = project.codebaseresources.status("compliance-licenses")
6059

6160
for codebase_resource in qs:
6261
scan_results, scan_errors = scancode.scan_file(codebase_resource.location)

scanpipe/pipes/docker.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,8 @@
2626
from pathlib import Path
2727

2828
from container_inspector.image import Image
29-
from container_inspector.rootfs import get_whiteout_marker_type
3029

3130
from scanpipe import pipes
32-
from scanpipe.models import CodebaseResource
3331
from scanpipe.pipes import rootfs
3432
from scanpipe.pipes import scancode
3533

@@ -133,7 +131,7 @@ def scan_image_for_system_packages(project, image, detect_licenses=True):
133131
missing_resources = created_package.missing_resources[:]
134132
modified_resources = created_package.modified_resources[:]
135133

136-
codebase_resources = CodebaseResource.objects.project(project)
134+
codebase_resources = project.codebaseresources.all()
137135

138136
for install_file in package.installed_files:
139137
install_file_path = pipes.normalize_path(install_file.path)
@@ -194,12 +192,10 @@ def scan_image_for_system_packages(project, image, detect_licenses=True):
194192

195193
def tag_whiteout_codebase_resources(project):
196194
"""
197-
Mark overlayfs/AUFS whiteout special files CodebaseResource as "ignored".
195+
Mark overlayfs/AUFS whiteout special files CodebaseResource as "ignored-whiteout".
196+
See https://github.com/opencontainers/image-spec/blob/master/layer.md#whiteouts
197+
for details.
198198
"""
199+
whiteout_prefix = ".wh."
199200
qs = project.codebaseresources.no_status()
200-
201-
for codebase_resource in qs:
202-
filename = Path(codebase_resource.path).name
203-
if get_whiteout_marker_type(filename):
204-
codebase_resource.status = "ignored-whiteout"
205-
codebase_resource.save()
201+
qs.filter(name__startswith=whiteout_prefix).update(status="ignored-whiteout")

0 commit comments

Comments
 (0)