Skip to content

Commit

Permalink
repository: Add a Resource cache to FileSetRepository
Browse files Browse the repository at this point in the history
The cache reduces the need to create new Resource objects, including
SHA-256 computation, for unchanged files.

Fixes bndtools#5367

Signed-off-by: BJ Hargrave <bj@hargrave.dev>
  • Loading branch information
bjhargrave committed Sep 19, 2022
1 parent e1eefb7 commit 133e3ea
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 54 deletions.
@@ -1,7 +1,5 @@
package aQute.bnd.repository.fileset;

import static aQute.bnd.exceptions.FunctionWithException.asFunctionOrElse;

import java.io.File;
import java.io.InputStream;
import java.net.URI;
Expand All @@ -10,10 +8,8 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.SortedSet;

import org.osgi.framework.namespace.IdentityNamespace;
import org.osgi.resource.Capability;
import org.osgi.resource.Requirement;
import org.osgi.resource.Resource;
Expand All @@ -24,23 +20,17 @@
import org.slf4j.LoggerFactory;

import aQute.bnd.exceptions.Exceptions;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.repository.BaseRepository;
import aQute.bnd.osgi.repository.BridgeRepository;
import aQute.bnd.osgi.repository.ResourcesRepository;
import aQute.bnd.osgi.resource.CapReqBuilder;
import aQute.bnd.osgi.resource.ResourceBuilder;
import aQute.bnd.osgi.resource.ResourceUtils;
import aQute.bnd.osgi.resource.ResourceUtils.ContentCapability;
import aQute.bnd.service.Plugin;
import aQute.bnd.service.Refreshable;
import aQute.bnd.service.RepositoryPlugin;
import aQute.bnd.util.repository.DownloadListenerPromise;
import aQute.bnd.version.MavenVersion;
import aQute.bnd.version.Version;
import aQute.lib.strings.Strings;
import aQute.maven.api.Revision;
import aQute.maven.provider.POM;
import aQute.service.reporter.Reporter;

public class FileSetRepository extends BaseRepository implements Plugin, RepositoryPlugin, Refreshable {
Expand All @@ -50,6 +40,7 @@ public class FileSetRepository extends BaseRepository implements Plugin, Reposit
private volatile Deferred<BridgeRepository> repository;
private Reporter reporter;
private final PromiseFactory promiseFactory;
private static final ResourceCache cache = new ResourceCache();

public FileSetRepository(String name, Collection<File> files) throws Exception {
this.name = name;
Expand Down Expand Up @@ -90,53 +81,14 @@ private Promise<BridgeRepository> readFiles() {
}

private Promise<Resource> parseFile(File file) {
Promise<Resource> resource = promiseFactory.submit(() -> {
if (!file.isFile()) {
return null;
}
ResourceBuilder rb = new ResourceBuilder();
try {
boolean hasIdentity = rb.addFile(file, null);
if (!hasIdentity) {
try (Jar jar = new Jar(file)) {
Optional<Revision> revision = jar.getPomXmlResources()
.findFirst()
.map(asFunctionOrElse(pomResource -> new POM(null, pomResource.openInputStream(), true),
null))
.map(POM::getRevision);

String name = jar.getModuleName();
if (name == null) {
name = revision.map(r -> r.program.toString())
.orElse(null);
if (name == null) {
return null;
}
}

Version version = revision.map(r -> r.version.getOSGiVersion())
.orElse(null);
if (version == null) {
version = new MavenVersion(jar.getModuleVersion()).getOSGiVersion();
}

CapReqBuilder identity = new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE)
.addAttribute(IdentityNamespace.IDENTITY_NAMESPACE, name)
.addAttribute(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE, version)
.addAttribute(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, IdentityNamespace.TYPE_UNKNOWN);
rb.addCapability(identity);
}
}
} catch (Exception f) {
return null;
}
logger.debug("{}: parsing {}", getName(), file);
return rb.build();
Promise<Resource> promise = promiseFactory.submit(() -> {
Resource resource = cache.getResource(this, file);
return resource;
});
if (logger.isDebugEnabled()) {
resource.onFailure(failure -> logger.debug("{}: failed to parse {}", getName(), file, failure));
promise.onFailure(failure -> logger.debug("{}: failed to parse {}", getName(), file, failure));
}
return resource;
return promise;
}

@Override
Expand Down
@@ -0,0 +1,93 @@
package aQute.bnd.repository.fileset;

import static aQute.bnd.exceptions.FunctionWithException.asFunctionOrElse;

import java.io.File;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import org.osgi.framework.namespace.IdentityNamespace;
import org.osgi.resource.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.resource.CapReqBuilder;
import aQute.bnd.osgi.resource.ResourceBuilder;
import aQute.bnd.service.RepositoryPlugin;
import aQute.bnd.version.MavenVersion;
import aQute.bnd.version.Version;
import aQute.maven.api.Revision;
import aQute.maven.provider.POM;

class ResourceCache {
private final static Logger logger = LoggerFactory
.getLogger(ResourceCache.class);
private final static long EXPIRED_DURATION_NANOS = TimeUnit.NANOSECONDS.convert(30L,
TimeUnit.MINUTES);
private final Map<ResourceCacheKey, Resource> cache;
private long time;

ResourceCache() {
cache = new ConcurrentHashMap<>();
time = System.nanoTime();
}

Resource getResource(RepositoryPlugin repo, File file) {
if (!file.isFile()) {
return null;
}
// Make sure we don't grow infinitely
final long now = System.nanoTime();
if ((now - time) > EXPIRED_DURATION_NANOS) {
cache.keySet()
.removeIf(key -> (now - key.time) > EXPIRED_DURATION_NANOS);
time = now;
}
ResourceCacheKey cacheKey = new ResourceCacheKey(file);
Resource resource = cache.computeIfAbsent(cacheKey, key -> {
ResourceBuilder rb = new ResourceBuilder();
try {
boolean hasIdentity = rb.addFile(file, null);
if (!hasIdentity) {
try (Jar jar = new Jar(file)) {
Optional<Revision> revision = jar.getPomXmlResources()
.findFirst()
.map(asFunctionOrElse(pomResource -> new POM(null, pomResource.openInputStream(), true),
null))
.map(POM::getRevision);

String name = jar.getModuleName();
if (name == null) {
name = revision.map(r -> r.program.toString())
.orElse(null);
if (name == null) {
return null;
}
}

Version version = revision.map(r -> r.version.getOSGiVersion())
.orElse(null);
if (version == null) {
version = new MavenVersion(jar.getModuleVersion()).getOSGiVersion();
}

CapReqBuilder identity = new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE)
.addAttribute(IdentityNamespace.IDENTITY_NAMESPACE, name)
.addAttribute(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE, version)
.addAttribute(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, IdentityNamespace.TYPE_UNKNOWN);
rb.addCapability(identity);
}
}
} catch (Exception f) {
return null;
}
logger.debug("{}: parsing {}", repo.getName(), file);
return rb.build();
});

return resource;
}
}
@@ -0,0 +1,64 @@
package aQute.bnd.repository.fileset;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.Objects;

import aQute.bnd.exceptions.Exceptions;

final class ResourceCacheKey {
private final Object fileKey;
private final FileTime lastAccessTime;
private final long size;
final long time;


ResourceCacheKey(File file) {
this(file.toPath());
}

ResourceCacheKey(Path path) {
BasicFileAttributes attributes;
try {
attributes = Files.getFileAttributeView(path, BasicFileAttributeView.class)
.readAttributes();
} catch (IOException e) {
throw Exceptions.duck(e);
}
if (!attributes.isRegularFile()) {
throw new IllegalArgumentException("File must be a regular file: " + path);
}
fileKey = attributes.fileKey();
lastAccessTime = attributes.lastAccessTime();
size = attributes.size();
time = System.nanoTime();
}

@Override
public int hashCode() {
return (Objects.hashCode(fileKey) * 31 + Objects.hashCode(lastAccessTime)) * 31 + Long.hashCode(size);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ResourceCacheKey)) {
return false;
}
ResourceCacheKey other = (ResourceCacheKey) obj;
return Objects.equals(fileKey, other.fileKey) && Objects.equals(lastAccessTime, other.lastAccessTime)
&& (size == other.size);
}

@Override
public String toString() {
return Objects.toString(fileKey);
}
}

0 comments on commit 133e3ea

Please sign in to comment.