Skip to content

Commit f651f8f

Browse files
authored
fix: improve performances and logging for SCIO importer (#546)
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 5f29f2b commit f651f8f

4 files changed

Lines changed: 63 additions & 16 deletions

File tree

compose.dev.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ services:
2222
web:
2323
build: .
2424
image: dejacode:dev
25-
env_file:
26-
- .env
2725
environment:
2826
- DATABASE_HOST=db
2927
- DATABASE_PASSWORD=dejacode
@@ -37,6 +35,7 @@ services:
3735
ports:
3836
- "8000:8000"
3937
volumes:
38+
- ./.env:/opt/dejacode/.env
4039
- ./component_catalog:/opt/dejacode/component_catalog
4140
- ./dejacode:/opt/dejacode/dejacode
4241
- ./dejacode_toolkit:/opt/dejacode/dejacode_toolkit

dejacode/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,11 @@ def get_fake_redis_connection(config, use_strict_redis):
586586
"propagate": False,
587587
"level": DEJACODE_LOG_LEVEL,
588588
},
589+
"product_portfolio": {
590+
"handlers": ["null"] if IS_TESTS else ["console"],
591+
"propagate": False,
592+
"level": "DEBUG" if DEBUG else DEJACODE_LOG_LEVEL,
593+
},
589594
},
590595
}
591596

@@ -717,6 +722,7 @@ def get_fake_redis_connection(config, use_strict_redis):
717722

718723
# Default to 5 seconds.
719724
DEJACODE_INTEGRATION_REQUESTS_TIMEOUT = env.int("DEJACODE_INTEGRATION_REQUESTS_TIMEOUT", default=5)
725+
CREATE_DEPENDENCIES_DEFAULT = env.bool("CREATE_DEPENDENCIES_DEFAULT", default=True)
720726

721727
if IS_TESTS:
722728
# Silent the django-axes logging during tests

product_portfolio/forms.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import json
1010

1111
from django import forms
12+
from django.conf import settings
1213
from django.core.exceptions import ValidationError
1314
from django.db import transaction
1415
from django.forms import BaseModelFormSet
@@ -557,7 +558,7 @@ class ImportFromScanForm(forms.Form):
557558
create_dependencies = forms.BooleanField(
558559
label=_("Create Dependencies"),
559560
required=False,
560-
initial=False,
561+
initial=settings.CREATE_DEPENDENCIES_DEFAULT,
561562
help_text=_(
562563
"When checked, dependency relationships between packages discovered in the "
563564
"import will be created on the Product."
@@ -664,7 +665,7 @@ class BaseProductImportFormView(forms.Form):
664665
create_dependencies = forms.BooleanField(
665666
label=_("Create Dependencies"),
666667
required=False,
667-
initial=False,
668+
initial=settings.CREATE_DEPENDENCIES_DEFAULT,
668669
help_text=_(
669670
"When checked, dependency relationships between packages discovered in the "
670671
"import will be created on the Product."
@@ -1002,7 +1003,7 @@ class PullProjectDataForm(forms.Form):
10021003
create_dependencies = forms.BooleanField(
10031004
label=_("Create Dependencies"),
10041005
required=False,
1005-
initial=False,
1006+
initial=settings.CREATE_DEPENDENCIES_DEFAULT,
10061007
help_text=_(
10071008
"When checked, dependency relationships between packages discovered in the "
10081009
"import will be created on the Product."

product_portfolio/importers.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
#
88

99
import json
10+
import logging
11+
import time
1012
from collections import defaultdict
13+
from contextlib import contextmanager
1114
from contextlib import suppress
1215

1316
from django import forms
@@ -48,6 +51,16 @@
4851
from product_portfolio.models import ProductRelationStatus
4952
from product_portfolio.models import ScanCodeProject
5053

54+
logger = logging.getLogger(__name__)
55+
56+
57+
@contextmanager
58+
def log_elapsed(label):
59+
"""Context manager that logs the elapsed time of the wrapped block."""
60+
start = time.perf_counter()
61+
yield
62+
logger.info(f"{label}: {time.perf_counter() - start:.1f}s")
63+
5164

5265
class CleanProductMixin(ComponentRelatedFieldImportMixin):
5366
def clean_product(self):
@@ -683,33 +696,57 @@ def __init__(
683696
self.package_uid_mapping = {}
684697

685698
self.user = user
699+
self.dataspace = product.dataspace
686700
self.project_uuid = project_uuid
687701
self.product = product
688702
self.update_existing = update_existing
689703
self.scan_all_packages = scan_all_packages
690704
self.infer_download_urls = infer_download_urls
691705
self.create_dependencies = create_dependencies
692706

693-
scancodeio = ScanCodeIO(user.dataspace)
694-
self.packages = scancodeio.fetch_project_packages(self.project_uuid)
695-
self.dependencies = scancodeio.fetch_project_dependencies(self.project_uuid)
696-
if not self.packages and not self.dependencies:
707+
def save(self):
708+
save_start = time.perf_counter()
709+
710+
scancodeio = ScanCodeIO(self.dataspace)
711+
712+
with log_elapsed("fetch_project_packages"):
713+
self.packages = scancodeio.fetch_project_packages(self.project_uuid)
714+
715+
if not self.packages:
697716
raise Exception("Packages could not be fetched from ScanCode.io")
698717

699-
def save(self):
700-
self.import_packages()
701718
if self.create_dependencies:
702-
self.import_dependencies()
719+
with log_elapsed("fetch_project_dependencies"):
720+
self.dependencies = scancodeio.fetch_project_dependencies(self.project_uuid)
721+
722+
with log_elapsed("import_packages"):
723+
self.import_packages()
724+
725+
if self.create_dependencies:
726+
with log_elapsed("import_dependencies"):
727+
self.import_dependencies()
703728

704729
if self.scan_all_packages:
705730
transaction.on_commit(lambda: self.product.scan_all_packages_task(self.user))
731+
logger.info("scan_all_packages: scheduled")
706732

707733
if self.user.dataspace.enable_vulnerablecodedb_access:
708-
self.product.fetch_vulnerabilities()
734+
with log_elapsed("fetch_vulnerabilities"):
735+
self.product.fetch_vulnerabilities()
736+
737+
logger.info(f"save total: {time.perf_counter() - save_start:.1f}s")
709738

710739
return dict(self.created), dict(self.existing), dict(self.errors)
711740

712741
def import_packages(self):
742+
# Pre-fetch once to avoid repeated DB lookups in DefaultOnAdditionMixin.save().
743+
self.default_review_status = ProductRelationStatus.objects.get_default_on_addition_qs(
744+
self.dataspace
745+
).first()
746+
self.default_purpose = ProductItemPurpose.objects.get_default_on_addition_qs(
747+
self.dataspace
748+
).first()
749+
713750
for package_data in self.packages:
714751
self.import_package(package_data)
715752

@@ -767,8 +804,9 @@ def import_package(self, package_data):
767804
if not package:
768805
try:
769806
package = Package.create_from_data(self.user, package_data, validate=True)
770-
except ValidationError as errors:
771-
self.errors["package"].append(str(errors))
807+
except ValidationError as error:
808+
logger.error(f"Failed to create package: {error}\n{package_data}")
809+
self.errors["package"].append(str(error))
772810
return
773811
self.created["package"].append(str(package))
774812

@@ -780,8 +818,11 @@ def import_package(self, package_data):
780818
"license_expression": package.license_expression,
781819
"notes": "Imported from ScanCode.io",
782820
"created_by": self.user,
821+
"review_status": self.default_review_status,
822+
"purpose": self.default_purpose,
783823
},
784824
)
825+
785826
package_uid = package_data.get("package_uid") or package.uuid
786827
self.package_uid_mapping[package_uid] = package
787828

@@ -846,7 +887,7 @@ def look_for_existing_package(self, package_data):
846887
return package
847888

848889
# 2. If the package data does not include a download_url value:
849-
# Attemp to find an existing package using purl-only match.
890+
# Attempt to find an existing package using purl-only match.
850891
if not package_data.get("download_url"):
851892
purl_lookups = {field: package_data.get(field, "") for field in PACKAGE_URL_FIELDS}
852893
same_purl_packages = package_qs.filter(**purl_lookups)

0 commit comments

Comments
 (0)