Skip to content

Commit

Permalink
ContikiMoteType: use arena for scope handling
Browse files Browse the repository at this point in the history
Add added/removed methods to the MoteType
interface, and call that from the simulation
when the motetype is added/removed.

Inside ContikiMoteType, replace the current
class-loader-tweaking with an arena that
is closed when the motetype is removed from
the simulation. This will close the shared
library as well.

This reduces the runtime for 07-simulation-base
from 1m 8s to 50s.
  • Loading branch information
pjonsson committed Aug 31, 2023
1 parent 519e540 commit babf6b3
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 250 deletions.
8 changes: 1 addition & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,6 @@ dependencies {

// FIXME: add test resources.
sourceSets {
corecomm {
resources {
srcDirs = ['java/org/contikios/cooja/corecomm']
}
}
data {
resources {
srcDirs = ['tools/coffee-manager']
Expand All @@ -57,10 +52,9 @@ sourceSets {
main {
java {
srcDirs = ['java', 'tools/coffee-manager']
exclude 'org/contikios/cooja/corecomm/CoreCommTemplate.java'
}
resources {
srcDirs = [corecomm.resources, data.resources, 'config']
srcDirs = [data.resources, 'config']
}
}
}
Expand Down
76 changes: 43 additions & 33 deletions java/org/contikios/cooja/CoreComm.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,48 +28,58 @@

package org.contikios.cooja;

import java.io.File;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.SegmentScope;
import java.lang.foreign.SymbolLookup;
import java.lang.invoke.MethodHandle;

/**
* The purpose of CoreComm is to communicate with a compiled Contiki system
* using Java Native Interface (JNI). Each implemented class (named
* Lib[number]), loads a shared library which belongs to one mote type. The
* reason for this somewhat strange design is that once loaded, a native library
* cannot be unloaded in Java (in the current versions available). Therefore, if
* we wish to load several libraries, the names and associated native functions
* must have unique names. And those names are defined via the calling class in
* JNI. For example, the corresponding function for a native tick method in
* class Lib1 will be named Java_org_contikios_cooja_corecomm_Lib1_tick. When creating
* a new mote type, the main Contiki source file is built with function
* names compatible with the next available corecomm class. This also implies
* that even if a mote type is deleted, a new one cannot be created using the
* same corecomm class without restarting the JVM and thus the entire
* simulation.
* <p>
* Each implemented CoreComm class needs read-access to the following core
* variables:
* <ul>
* <li>referenceVar
* </ul>
* and the following native functions:
* <ul>
* <li>tick()
* <li>init()
* <li>getReferenceAbsAddr()
* <li>getMemory(int start, int length, byte[] mem)
* <li>setMemory(int start, int length, byte[] mem)
* </ul>
*
* @author Fredrik Osterlind
* CoreComm loads a library file and keeps track of the method handles
* to the tick and getReferenceAddress methods.
*/
public interface CoreComm {
// Do not bother end-user with warnings about internal Cooja details.
@SuppressWarnings("preview")
public class CoreComm {
private final SymbolLookup symbols;
private final MethodHandle coojaTick;
/**
* Loads library libFile with a scope.
*
* @param scope Scope to load the library file in
* @param libFile Library file
*/
public CoreComm(SegmentScope scope, File libFile) {
symbols = SymbolLookup.libraryLookup(libFile.getAbsolutePath(), scope);
var linker = Linker.nativeLinker();
coojaTick = linker.downcallHandle(symbols.find("cooja_tick").get(),
FunctionDescriptor.ofVoid());
// Call cooja_init() in Contiki-NG.
var coojaInit = linker.downcallHandle(symbols.find("cooja_init").get(),
FunctionDescriptor.ofVoid());
try {
coojaInit.invokeExact();
} catch (Throwable e) {
throw new RuntimeException("Calling cooja_init failed: " + e.getMessage(), e);
}
}

/**
* Ticks a mote once.
*/
void tick();
public void tick() {
try {
coojaTick.invokeExact();
} catch (Throwable e) {
throw new RuntimeException("Calling cooja_tick failed: " + e.getMessage(), e);
}
}

/**
* Returns the absolute memory address of the reference variable.
*/
long getReferenceAddress();
public long getReferenceAddress() {
return symbols.find("referenceVar").get().address();
}
}
6 changes: 6 additions & 0 deletions java/org/contikios/cooja/MoteType.java
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ default long getExecutableAddressOf(File file, int lineNr) {
return -1;
}

/** Called when the mote type is added to the simulation. */
default void added() { }

/** Called when the mote type is removed from the simulation. */
default void removed() { }

class MoteTypeCreationException extends Exception {
private MessageList compilationOutput;

Expand Down
7 changes: 2 additions & 5 deletions java/org/contikios/cooja/Simulation.java
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,7 @@ public MoteType[] getMoteTypes() {
public void addMoteType(MoteType newMoteType) {
Cooja.usedMoteTypeIDs.add(newMoteType.getIdentifier());
moteTypes.add(newMoteType);
newMoteType.added();
moteTypeTriggers.trigger(AddRemove.ADD, newMoteType);
}

Expand All @@ -897,14 +898,10 @@ public void removeMoteType(MoteType type) {
removeMote(m);
}
}

// FIXME: call type.removed(), so ContikiMoteType can call arena.close()
// to unload the shared library.
// For more details, see "Obtaining a symbol lookup" at:
// https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/foreign/SymbolLookup.html
if (moteTypes.remove(type)) {
moteTypeTriggers.trigger(AddRemove.REMOVE, type);
}
type.removed();
}

/**
Expand Down
123 changes: 7 additions & 116 deletions java/org/contikios/cooja/contikimote/ContikiMoteType.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,11 @@

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SegmentScope;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
Expand All @@ -47,7 +44,6 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -113,10 +109,9 @@
// Do not bother end-user with warnings about internal Cooja details.
@SuppressWarnings("preview")
public class ContikiMoteType extends BaseContikiMoteType {

private static final Logger logger = LoggerFactory.getLogger(ContikiMoteType.class);
private static int fileCounter = 1;

// Shared Arena since MoteTypes are allocated/removed in different threads.
private final Arena arena = Arena.openShared();
/**
* Communication stacks in Contiki.
*/
Expand Down Expand Up @@ -260,7 +255,7 @@ public boolean loadMoteFirmware(boolean vis) throws MoteTypeCreationException {

// Allocate core communicator class
final var firmwareFile = getContikiFirmwareFile();
myCoreComm = createCoreComm(tmpDir, firmwareFile);
myCoreComm = new CoreComm(arena.scope(), firmwareFile);

/* Parse addresses using map file
* or output of command specified in external tools settings (e.g. nm -a )
Expand Down Expand Up @@ -788,112 +783,8 @@ public boolean setConfigXML(Simulation simulation,
return configureAndInit(Cooja.getTopParentContainer(), simulation, Cooja.isVisualized());
}

/**
* Generates new source file by reading default source template and replacing
* the class name field.
*
* @param tempDir Directory for temporary files
* @param className Java class name (without extension)
* @throws MoteTypeCreationException If error occurs
*/
private static void generateLibSourceFile(Path tempDir, String className) throws MoteTypeCreationException {
// Create the temporary directory and ensure it is deleted on exit.
File dir = tempDir.toFile();
StringBuilder path = new StringBuilder(tempDir.toString());
// Gradually do mkdir() since mkdirs() makes deleteOnExit() leave the
// directory when Cooja quits.
for (String p : new String[]{"/org", "/contikios", "/cooja", "/corecomm"}) {
path.append(p);
dir = new File(path.toString());
if (!dir.mkdir()) {
throw new MoteTypeCreationException("Could not create temporary directory: " + dir);
}
dir.deleteOnExit();
}
Path dst = Path.of(dir + "/" + className + ".java");
dst.toFile().deleteOnExit();

// Instantiate the CoreComm template into the temporary directory.
try (var input = ContikiMoteType.class.getResourceAsStream('/' + "CoreCommTemplate.java");
var reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(input), UTF_8));
var writer = Files.newBufferedWriter(dst, UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line.replace("CoreCommTemplate", className) + "\n");
}
} catch (Exception e) {
throw new MoteTypeCreationException("Could not generate corecomm source file: " + className + ".java", e);
}
}

/**
* Compiles Java class.
*
* @param tempDir Directory for temporary files
* @param className Java class name (without extension)
* @throws MoteTypeCreationException If Java class compilation error occurs
*/
private static void compileSourceFile(Path tempDir, String className) throws MoteTypeCreationException {
String[] cmd = {Cooja.configuration.javac(),
"-cp", System.getProperty("java.class.path"), "--release", String.valueOf(Runtime.version().feature()),
"--enable-preview",
tempDir + "/org/contikios/cooja/corecomm/" + className + ".java" };
ProcessBuilder pb = new ProcessBuilder(cmd);
Process p;
try {
p = pb.start();
} catch (IOException e) {
throw new MoteTypeCreationException("Could not start Java compiler: " + cmd[0], e);
}

// Try to create a message list with support for GUI - will give not UI if headless.
var compilationOutput = MessageContainer.createMessageList(true);
var stdout = compilationOutput.getInputStream(MessageList.NORMAL);
var stderr = compilationOutput.getInputStream(MessageList.ERROR);
try (var outputStream = p.inputReader(UTF_8);
var errorStream = p.errorReader(UTF_8)) {
int b;
while ((b = outputStream.read()) >= 0) {
stdout.write(b);
}
while ((b = errorStream.read()) >= 0) {
stderr.write(b);
}
if (p.waitFor() == 0) {
File classFile = new File(tempDir + "/org/contikios/cooja/corecomm/" + className + ".class");
classFile.deleteOnExit();
return;
}
} catch (IOException | InterruptedException e) {
throw new MoteTypeCreationException("Could not compile corecomm source file: " + className + ".java", e, compilationOutput);
}
throw new MoteTypeCreationException("Could not compile corecomm source file: " + className + ".java", compilationOutput);
}

/**
* Create and return an instance of the core communicator identified by
* className. This core communicator will load the native library libFile.
*
* @param tempDir Directory for temporary files
* @param libFile Native library file
* @return Core Communicator
*/
private static CoreComm createCoreComm(Path tempDir, File libFile) throws MoteTypeCreationException {
// Loading a class might leave residue in the JVM so use a new name for the next call.
final var className = "Lib" + fileCounter++;
generateLibSourceFile(tempDir, className);
compileSourceFile(tempDir, className);
Class<? extends CoreComm> newCoreCommClass;
try (var loader = URLClassLoader.newInstance(new URL[]{tempDir.toUri().toURL()})) {
newCoreCommClass = loader.loadClass("org.contikios.cooja.corecomm." + className).asSubclass(CoreComm.class);
} catch (IOException | NullPointerException | ClassNotFoundException e1) {
throw new MoteTypeCreationException("Could not load corecomm class file: " + className + ".class", e1);
}

try {
return newCoreCommClass.getConstructor(File.class).newInstance(libFile);
} catch (Exception e) {
throw new MoteTypeCreationException("Error when creating corecomm instance: " + className, e);
}
@Override
public void removed() {
arena.close();
}
}
Loading

0 comments on commit babf6b3

Please sign in to comment.