com.caoccao.javet.interop.V8Host Maven / Gradle / Ivy
/*
* Copyright (c) 2021-2024. caoccao.com Sam Cao
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.caoccao.javet.interop;
import com.caoccao.javet.enums.JSRuntimeType;
import com.caoccao.javet.exceptions.JavetError;
import com.caoccao.javet.exceptions.JavetException;
import com.caoccao.javet.interfaces.IJavetLogger;
import com.caoccao.javet.interop.loader.JavetLibLoader;
import com.caoccao.javet.interop.monitoring.V8SharedMemoryStatistics;
import com.caoccao.javet.interop.monitoring.V8StatisticsFuture;
import com.caoccao.javet.interop.options.RuntimeOptions;
import com.caoccao.javet.utils.JavetDateTimeUtils;
import com.caoccao.javet.utils.JavetDefaultLogger;
import com.caoccao.javet.utils.SimpleMap;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryType;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* The type V8 host.
*
* @since 0.7.0
*/
@SuppressWarnings("unchecked")
public final class V8Host {
private static final long INVALID_HANDLE = 0L;
private static final Map> v8StatisticsFutureMap = new HashMap<>(1024);
private static final Object v8StatisticsFutureMapLock = new Object();
private static boolean libraryReloadable = false;
private static volatile double memoryUsageThresholdRatio = 0.7;
private final JSRuntimeType jsRuntimeType;
private final IJavetLogger logger;
private final V8GuardDaemon v8GuardDaemon;
private final V8Notifier v8Notifier;
private final ConcurrentHashMap v8RuntimeMap;
private final V8StatisticsFutureDaemon v8StatisticsFutureDaemon;
private boolean isolateCreated;
private JavetClassLoader javetClassLoader;
private JavetException lastException;
private volatile boolean libraryLoaded;
private Thread threadV8GuardDaemon;
private Thread threadV8StatisticsFutureDaemon;
private IV8Native v8Native;
private V8Host(JSRuntimeType jsRuntimeType) {
Objects.requireNonNull(jsRuntimeType);
javetClassLoader = null;
lastException = null;
libraryLoaded = false;
logger = new JavetDefaultLogger(getClass().getName());
v8RuntimeMap = new ConcurrentHashMap<>();
v8Native = null;
isolateCreated = false;
this.jsRuntimeType = jsRuntimeType;
v8GuardDaemon = new V8GuardDaemon();
v8StatisticsFutureDaemon = new V8StatisticsFutureDaemon();
threadV8GuardDaemon = null;
threadV8StatisticsFutureDaemon = null;
loadLibrary();
v8Notifier = new V8Notifier(v8RuntimeMap);
}
/**
* Gets instance by JS runtime type.
*
* @param jsRuntimeType the JS runtime type
* @return the instance
* @since 0.7.0
*/
public static V8Host getInstance(JSRuntimeType jsRuntimeType) {
if (Objects.requireNonNull(jsRuntimeType).isNode()) {
return getNodeInstance();
}
return getV8Instance();
}
/**
* Gets memory usage threshold ratio.
*
* @return the memory usage threshold ratio
* @since 0.8.3
*/
public static double getMemoryUsageThresholdRatio() {
return memoryUsageThresholdRatio;
}
/**
* Gets Node instance.
*
* Note: Node runtime library is loaded by a custom class loader.
*
* @return the Node instance
* @since 0.8.0
*/
public static V8Host getNodeInstance() {
return NodeInstanceHolder.INSTANCE;
}
/**
* Gets V8 instance.
*
* Note: V8 runtime library is loaded by a custom class loader.
*
* @return the V8 instance
* @since 0.8.0
*/
public static V8Host getV8Instance() {
return V8InstanceHolder.INSTANCE;
}
/**
* Determines whether the JNI library is reloadable or not.
*
* @return true : reloadable, false: not reloadable, default: false
* @since 0.9.1
*/
public static boolean isLibraryReloadable() {
return libraryReloadable;
}
private static void purgeV8StatisticsFuture(V8StatisticsFuture> v8StatisticsFuture, IV8Native iV8Native) {
synchronized (v8StatisticsFutureMapLock) {
if (v8StatisticsFutureMap.remove(v8StatisticsFuture.getHandle()) != null) {
iV8Native.removeRawPointer(
v8StatisticsFuture.getHandle(),
v8StatisticsFuture.getRawPointerType().getId());
}
}
}
private static void registerV8StatisticsFuture(V8StatisticsFuture> v8StatisticsFuture) {
synchronized (v8StatisticsFutureMapLock) {
v8StatisticsFutureMap.put(v8StatisticsFuture.getHandle(), v8StatisticsFuture);
}
}
private static boolean requestV8StatisticsFuture(long handle) {
synchronized (v8StatisticsFutureMapLock) {
return v8StatisticsFutureMap.remove(handle) != null;
}
}
/**
* Sets whether the JNI library is reloadable or not.
*
* @param libraryReloadable true: reloadable, false: not reloadable
* @since 0.9.1
*/
public static void setLibraryReloadable(boolean libraryReloadable) {
V8Host.libraryReloadable = libraryReloadable;
}
private static void setMemoryUsageThreshold() {
/*
* @see Memory Usage Monitoring
*/
/* if not defined ANDROID */
if (memoryUsageThresholdRatio > 0) {
MemoryPoolMXBean heapMemoryPoolMXBean = null;
List memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans();
for (MemoryPoolMXBean memoryPoolMXBean : memoryPoolMXBeans) {
if (memoryPoolMXBean.getType() == MemoryType.HEAP && memoryPoolMXBean.isUsageThresholdSupported()) {
heapMemoryPoolMXBean = memoryPoolMXBean;
break;
}
}
if (heapMemoryPoolMXBean != null) {
final long memoryUsageThreshold = (long) Math.floor(
heapMemoryPoolMXBean.getUsage().getMax() * memoryUsageThresholdRatio);
heapMemoryPoolMXBean.setUsageThreshold(memoryUsageThreshold);
}
}
/* end if */
}
/**
* Sets memory usage threshold ratio.
*
* This manageable usage threshold attribute is designed for monitoring
* the increasing trend of memory usage with low overhead.
*
* @param memoryUsageThresholdRatio the memory usage threshold ratio
* @since 0.8.3
*/
public static void setMemoryUsageThresholdRatio(double memoryUsageThresholdRatio) {
assert 0 <= memoryUsageThresholdRatio && memoryUsageThresholdRatio < 1;
V8Host.memoryUsageThresholdRatio = memoryUsageThresholdRatio;
}
/**
* Clear internal statistic for internal test purpose.
*
* @since 0.8.3
*/
public void clearInternalStatistic() {
v8Native.clearInternalStatistic();
}
/**
* Close V8 runtime.
*
* @param v8Runtime the V8 runtime
* @since 0.7.0
*/
public void closeV8Runtime(V8Runtime v8Runtime) {
if (!libraryLoaded) {
return;
}
if (v8Runtime != null) {
final long handle = v8Runtime.getHandle();
if (handle != INVALID_HANDLE && v8RuntimeMap.containsKey(handle)) {
v8Native.closeV8Runtime(handle);
v8RuntimeMap.remove(handle);
}
}
}
/**
* Create V8 runtime.
*
* @param the type parameter
* @return the V8 runtime
* @throws JavetException the javet exception
* @since 0.7.0
*/
public R createV8Runtime() throws JavetException {
return createV8Runtime(getJSRuntimeType().getRuntimeOptions());
}
/**
* Create V8 runtime by custom global name.
*
* @param the type parameter
* @param runtimeOptions the runtime options
* @return the V8 runtime
* @throws JavetException the javet exception
* @since 0.7.0
*/
public R createV8Runtime(RuntimeOptions> runtimeOptions) throws JavetException {
return createV8Runtime(false, runtimeOptions);
}
/**
* Create V8 runtime by pooled flag and custom global name.
*
* @param the type parameter
* @param pooled the pooled
* @param runtimeOptions the runtime options
* @return the V8 runtime
* @throws JavetException the javet exception
* @since 0.7.0
*/
public R createV8Runtime(
boolean pooled, RuntimeOptions> runtimeOptions) throws JavetException {
assert getJSRuntimeType().isRuntimeOptionsValid(runtimeOptions);
if (!libraryLoaded) {
if (lastException == null) {
throw new JavetException(
JavetError.LibraryNotLoaded,
SimpleMap.of(JavetError.PARAMETER_REASON, "there are unknown errors"));
} else {
throw lastException;
}
}
final long handle = v8Native.createV8Runtime(runtimeOptions);
isolateCreated = true;
V8Runtime v8Runtime;
if (jsRuntimeType.isNode()) {
v8Runtime = new NodeRuntime(this, handle, pooled, v8Native, runtimeOptions);
} else {
v8Runtime = new V8Runtime(this, handle, pooled, v8Native, runtimeOptions);
}
v8Native.registerV8Runtime(handle, v8Runtime);
v8RuntimeMap.put(handle, v8Runtime);
return (R) v8Runtime;
}
/**
* Disable GC notification.
*
* @return the self
* @since 0.8.3
*/
@SuppressWarnings("UnusedReturnValue")
public V8Host disableGCNotification() {
v8Notifier.unregisterListener();
return this;
}
/**
* Enable GC notification.
*
* @return the self
* @since 0.8.3
*/
public V8Host enableGCNotification() {
setMemoryUsageThreshold();
// Javet {@link V8Notifier} listens to this notification to notify {@link V8Runtime} to perform GC.
v8Notifier.registerListeners();
return this;
}
/**
* Get internal statistic for test purpose.
*
* @return the internal statistic
* @since 0.8.3
*/
public long[] getInternalStatistic() {
return v8Native.getInternalStatistic();
}
/**
* Gets JS runtime type.
*
* @return the JS runtime type
* @since 0.8.0
*/
public JSRuntimeType getJSRuntimeType() {
return jsRuntimeType;
}
/**
* Gets javet version.
*
* @return the javet version
* @since 0.7.1
*/
public String getJavetVersion() {
return JavetLibLoader.LIB_VERSION;
}
/**
* Gets last exception.
*
* @return the last exception
* @since 0.7.0
*/
public JavetException getLastException() {
return lastException;
}
/**
* Gets logger.
*
* @return the logger
* @since 0.7.3
*/
public IJavetLogger getLogger() {
return logger;
}
/**
* Gets sleep interval millis.
*
* @return the sleep interval millis
* @since 3.1.3
*/
public long getSleepIntervalMillis() {
return v8GuardDaemon.getSleepIntervalMillis();
}
/**
* Gets V8 guard daemon.
*
* @return the V8 guard daemon
* @since 3.1.3
*/
V8GuardDaemon getV8GuardDaemon() {
return v8GuardDaemon;
}
/**
* Gets V8 native.
*
* @return the V8 native
* @since 0.8.0
*/
IV8Native getV8Native() {
return v8Native;
}
/**
* Gets V8 runtime count.
*
* @return the V8 runtime count
* @since 0.8.0
*/
public int getV8RuntimeCount() {
return v8RuntimeMap.size();
}
/**
* Gets V8 shared memory statistics.
*
* @return the V8 shared memory statistics
* @since 1.0.6
*/
public V8SharedMemoryStatistics getV8SharedMemoryStatistics() {
return (V8SharedMemoryStatistics) v8Native.getV8SharedMemoryStatistics();
}
/**
* Is isolate created.
*
* @return true : created, false: not created
* @since 0.8.0
*/
public boolean isIsolateCreated() {
return isolateCreated;
}
/**
* Is library loaded.
*
* @return true : loaded, false: not loaded
* @since 0.8.0
*/
public boolean isLibraryLoaded() {
return libraryLoaded;
}
/**
* Load library.
*
* Note: setLibraryReloadable(true) must be called, otherwise, JVM will crash.
*
* @return true : library is loaded, false: library is not loaded
* @since 0.9.1
*/
public synchronized boolean loadLibrary() {
if (!libraryLoaded) {
try {
logger.logDebug(
"[{0}] Loading library.",
jsRuntimeType.getName());
javetClassLoader = new JavetClassLoader(getClass().getClassLoader(), jsRuntimeType);
javetClassLoader.load();
v8Native = javetClassLoader.getNative();
libraryLoaded = true;
isolateCreated = false;
threadV8GuardDaemon = new Thread(v8GuardDaemon);
threadV8GuardDaemon.setDaemon(true);
threadV8GuardDaemon.start();
v8StatisticsFutureDaemon.setV8Native(v8Native);
threadV8StatisticsFutureDaemon = new Thread(v8StatisticsFutureDaemon);
threadV8StatisticsFutureDaemon.setDaemon(true);
threadV8StatisticsFutureDaemon.start();
} catch (JavetException e) {
logger.logError(e, "Failed to load Javet lib with error {0}.", e.getMessage());
lastException = e;
}
}
return libraryLoaded;
}
/**
* Offer V8 statistics future to the queue.
*
* @param v8StatisticsFuture the V8 statistics future
*/
void offerV8StatisticsFuture(V8StatisticsFuture> v8StatisticsFuture) {
v8StatisticsFutureDaemon.getV8StatisticsFutureQueue().offer(Objects.requireNonNull(v8StatisticsFuture));
}
/**
* Sets sleep interval millis.
*
* @param sleepIntervalMillis the sleep interval millis
* @since 3.1.3
*/
public void setSleepIntervalMillis(long sleepIntervalMillis) {
v8GuardDaemon.setSleepIntervalMillis(sleepIntervalMillis);
}
/**
* Unload library.
*
* Note: setLibraryReloadable(true) must be called, otherwise, JVM will crash.
*
* @return true : library is unloaded, false: library is loaded
* @since 0.9.1
*/
public synchronized boolean unloadLibrary() {
if (libraryLoaded && v8RuntimeMap.isEmpty()) {
logger.logDebug(
"[{0}] Unloading library.",
jsRuntimeType.getName());
threadV8GuardDaemon.interrupt();
threadV8GuardDaemon = null;
v8GuardDaemon.getV8GuardQueue().clear();
threadV8StatisticsFutureDaemon.interrupt();
threadV8StatisticsFutureDaemon = null;
v8StatisticsFutureDaemon.purgeV8StatisticsFutureQueue();
v8StatisticsFutureDaemon.setV8Native(null);
isolateCreated = false;
v8Native = null;
javetClassLoader = null;
System.gc();
System.runFinalization();
libraryLoaded = false;
lastException = null;
}
return !libraryLoaded;
}
private static class NodeInstanceHolder {
private static final V8Host INSTANCE = new V8Host(JSRuntimeType.Node);
}
static class V8GuardDaemon implements Runnable {
private static final long DEFAULT_SLEEP_INTERVAL_MILLIS = 5;
private static final int INITIAL_CAPACITY = 64;
private static final boolean IS_IN_DEBUG_MODE =
/* if defined ANDROID
false;
/* end if */
/* if not defined ANDROID */
ManagementFactory.getRuntimeMXBean().getInputArguments().toString().indexOf("-agentlib:jdwp") > 0;
/* end if */
private final PriorityBlockingQueue v8GuardQueue;
private long sleepIntervalMillis;
public V8GuardDaemon() {
sleepIntervalMillis = DEFAULT_SLEEP_INTERVAL_MILLIS;
v8GuardQueue = new PriorityBlockingQueue<>(
INITIAL_CAPACITY,
(g1, g2) -> (int) (g1.getEndTimeMillis() - g2.getEndTimeMillis()));
}
public long getSleepIntervalMillis() {
return sleepIntervalMillis;
}
public PriorityBlockingQueue getV8GuardQueue() {
return v8GuardQueue;
}
@Override
public void run() {
while (true) {
try {
V8Guard v8Guard = v8GuardQueue.take();
long now = System.currentTimeMillis();
if (now > v8Guard.getEndTimeMillis()) {
if (!(!v8Guard.isDebugModeEnabled() && IS_IN_DEBUG_MODE)) {
V8Runtime v8Runtime = v8Guard.getV8Runtime();
synchronized (v8Runtime.getCloseLock()) {
if (!v8Guard.isClosed() && !v8Runtime.isClosed() && v8Runtime.isInUse()) {
v8Runtime.terminateExecution();
v8Runtime.getLogger().logWarn(
"Execution was terminated after {0}ms.",
now - v8Guard.getStartTimeMillis());
}
}
}
} else {
V8Runtime v8Runtime = v8Guard.getV8Runtime();
synchronized (v8Runtime.getCloseLock()) {
if (!v8Guard.isClosed() && !v8Runtime.isClosed()) {
v8GuardQueue.add(v8Guard);
long sleepMillis = Math.min(v8Guard.getEndTimeMillis() - now, sleepIntervalMillis);
if (sleepMillis > 0) {
TimeUnit.MILLISECONDS.sleep(sleepMillis);
}
}
}
}
} catch (InterruptedException ignored) {
break;
}
}
}
public void setSleepIntervalMillis(long sleepIntervalMillis) {
assert sleepIntervalMillis > 0 : "sleepIntervalMillis must be greater than 0";
this.sleepIntervalMillis = sleepIntervalMillis;
}
}
private static class V8InstanceHolder {
private static final V8Host INSTANCE = new V8Host(JSRuntimeType.V8);
}
static class V8StatisticsFutureDaemon implements Runnable {
private static final long SLEEP_IN_MILLIS = 1000;
private static final long TIMEOUT_IN_SECONDS = 60;
private final ConcurrentLinkedQueue> v8StatisticsFutureQueue;
private IV8Native v8Native;
public V8StatisticsFutureDaemon() {
v8Native = null;
v8StatisticsFutureQueue = new ConcurrentLinkedQueue<>();
}
public ConcurrentLinkedQueue> getV8StatisticsFutureQueue() {
return v8StatisticsFutureQueue;
}
public void purgeV8StatisticsFutureQueue() {
while (!v8StatisticsFutureQueue.isEmpty()) {
V8StatisticsFuture> v8StatisticsFuture = v8StatisticsFutureQueue.poll();
if (!v8StatisticsFuture.isDone()) {
V8Host.purgeV8StatisticsFuture(v8StatisticsFuture, v8Native);
}
}
}
@Override
public void run() {
while (true) {
try {
if (v8StatisticsFutureQueue.isEmpty()) {
TimeUnit.MILLISECONDS.sleep(SLEEP_IN_MILLIS);
} else {
V8StatisticsFuture> v8StatisticsFuture = v8StatisticsFutureQueue.peek();
if (v8StatisticsFuture.isDone()) {
v8StatisticsFutureQueue.poll();
} else {
ZonedDateTime now = JavetDateTimeUtils.getUTCNow();
ZonedDateTime purgeDateTime =
v8StatisticsFuture.getCreationDateTime().plusSeconds(TIMEOUT_IN_SECONDS);
Duration duration = Duration.between(now, purgeDateTime);
if (duration.isNegative()) {
V8Host.purgeV8StatisticsFuture(v8StatisticsFuture, v8Native);
v8StatisticsFutureQueue.poll();
} else {
TimeUnit.MILLISECONDS.sleep(duration.toMillis());
}
}
}
} catch (InterruptedException ignored) {
break;
}
}
}
public void setV8Native(IV8Native v8Native) {
this.v8Native = v8Native;
}
}
}