Skip to content

Commit b0bb6df

Browse files
EcljpseB0Tjukzi
authored andcommitted
API: add IWorkspace.write(Map<IFile, byte[]> ...)
to create multiple IFile in a batch. For example during clean-build JDT first deletes all output folders and then writes one .class file after the other. Typically many files are written sequentially. However they could be written in parallel if there would be an API. This change keeps all changes to the workspace single threaded but forwards the IO of creating multiple files to multiple threads. The single most important use case would be JDT's AbstractImageBuilder.writeClassFileContents() The speedup on windows is ~ number of cores, when they have hyper threading. OutOfMemory is not to be feared as the caller has full control how many bytes he passes.
1 parent 7d441c9 commit b0bb6df

File tree

4 files changed

+303
-3
lines changed

4 files changed

+303
-3
lines changed

resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/File.java

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
import java.io.IOException;
2222
import java.io.InputStream;
2323
import java.io.Reader;
24+
import java.util.ArrayList;
25+
import java.util.List;
26+
import java.util.Map.Entry;
27+
import java.util.concurrent.ConcurrentMap;
28+
import java.util.concurrent.ExecutionException;
29+
import java.util.concurrent.ExecutorService;
30+
import java.util.concurrent.Future;
2431
import org.eclipse.core.filesystem.EFS;
2532
import org.eclipse.core.filesystem.IFileInfo;
2633
import org.eclipse.core.filesystem.IFileStore;
@@ -38,6 +45,7 @@
3845
import org.eclipse.core.runtime.CoreException;
3946
import org.eclipse.core.runtime.IPath;
4047
import org.eclipse.core.runtime.IProgressMonitor;
48+
import org.eclipse.core.runtime.NullProgressMonitor;
4149
import org.eclipse.core.runtime.OperationCanceledException;
4250
import org.eclipse.core.runtime.Platform;
4351
import org.eclipse.core.runtime.QualifiedName;
@@ -203,15 +211,15 @@ public void create(byte[] content, int updateFlags, IProgressMonitor monitor) th
203211
}
204212
}
205213

206-
private void checkCreatable() throws CoreException {
214+
void checkCreatable() throws CoreException {
207215
checkDoesNotExist();
208216
Container parent = (Container) getParent();
209217
ResourceInfo info = parent.getResourceInfo(false, false);
210218
parent.checkAccessible(getFlags(info));
211219
checkValidGroupContainer(parent, false, false);
212220
}
213221

214-
private IFileInfo create(int updateFlags, SubMonitor subMonitor, IFileStore store)
222+
IFileInfo create(int updateFlags, IProgressMonitor subMonitor, IFileStore store)
215223
throws CoreException, ResourceException {
216224
String message;
217225
IFileInfo localInfo;
@@ -391,6 +399,60 @@ protected void internalSetContents(byte[] content, IFileInfo fileInfo, int updat
391399
updateMetadataFiles();
392400
workspace.getAliasManager().updateAliases(this, getStore(), IResource.DEPTH_ZERO, monitor);
393401
}
402+
403+
static void internalSetMultipleContents(ConcurrentMap<File, byte[]> filesToCreate, int updateFlags, boolean append,
404+
IProgressMonitor monitor, ExecutorService executorService) throws CoreException {
405+
SubMonitor subMonitor = SubMonitor.convert(monitor, filesToCreate.size());
406+
List<Future<CoreException>> futures = new ArrayList<>(filesToCreate.size());
407+
for (Entry<File, byte[]> e : filesToCreate.entrySet()) {
408+
Future<CoreException> future = executorService.submit(() -> {
409+
try {
410+
File file = e.getKey();
411+
byte[] content = e.getValue();
412+
writeSingle(updateFlags, append, subMonitor.slice(1), file, content);
413+
} catch (CoreException ce) {
414+
return ce;
415+
}
416+
return null;
417+
});
418+
futures.add(future);
419+
}
420+
CoreException ex = null;
421+
for (Future<CoreException> f : futures) {
422+
CoreException ce;
423+
try {
424+
ce = f.get();
425+
} catch (InterruptedException | ExecutionException e) {
426+
ce = new CoreException(Status.error("Error during parallel IO", e)); //$NON-NLS-1$
427+
}
428+
if (ce != null) {
429+
if (ex == null) {
430+
ex = ce;
431+
} else {
432+
ex.addSuppressed(ce);
433+
}
434+
}
435+
}
436+
if (ex != null) {
437+
ex.addSuppressed(new IllegalStateException("Stacktrace of invoking parallel IO")); //$NON-NLS-1$
438+
throw ex;
439+
}
440+
NullProgressMonitor npm = new NullProgressMonitor();
441+
for (File file : filesToCreate.keySet()) {
442+
file.updateMetadataFiles();
443+
file.workspace.getAliasManager().updateAliases(file, file.getStore(), IResource.DEPTH_ZERO, npm);
444+
file.setLocal(true);
445+
}
446+
}
447+
448+
private static void writeSingle(int updateFlags, boolean append, IProgressMonitor monitor, File file,
449+
byte[] content) throws CoreException, ResourceException {
450+
IFileStore store = file.getStore();
451+
NullProgressMonitor npm = new NullProgressMonitor();
452+
IFileInfo localInfo = file.create(updateFlags, npm, store);
453+
file.getLocalManager().write(file, content, localInfo, updateFlags, append, monitor);
454+
}
455+
394456
/**
395457
* Optimized refreshLocal for files. This implementation does not block the workspace
396458
* for the common case where the file exists both locally and on the file system, and

resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Workspace.java

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,15 @@
3838
import java.util.LinkedList;
3939
import java.util.List;
4040
import java.util.Map;
41+
import java.util.Map.Entry;
42+
import java.util.Objects;
4143
import java.util.Set;
4244
import java.util.SortedSet;
4345
import java.util.TreeSet;
46+
import java.util.concurrent.ConcurrentHashMap;
47+
import java.util.concurrent.ConcurrentMap;
4448
import java.util.concurrent.CopyOnWriteArrayList;
49+
import java.util.concurrent.ExecutorService;
4550
import java.util.concurrent.atomic.AtomicLong;
4651
import java.util.function.Predicate;
4752
import org.eclipse.core.filesystem.EFS;
@@ -117,6 +122,7 @@
117122
import org.eclipse.core.runtime.jobs.ISchedulingRule;
118123
import org.eclipse.core.runtime.jobs.Job;
119124
import org.eclipse.core.runtime.jobs.JobGroup;
125+
import org.eclipse.core.runtime.jobs.MultiRule;
120126
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
121127
import org.eclipse.core.runtime.preferences.InstanceScope;
122128
import org.eclipse.osgi.util.NLS;
@@ -2790,4 +2796,92 @@ public IStatus validateFiltered(IResource resource) {
27902796
}
27912797
return Status.OK_STATUS;
27922798
}
2799+
2800+
@Override
2801+
public void write(Map<IFile, byte[]> contentMap, boolean force, boolean derived, boolean keepHistory,
2802+
IProgressMonitor monitor, ExecutorService executorService) throws CoreException {
2803+
Objects.requireNonNull(contentMap);
2804+
ConcurrentMap<File, byte[]> filesToCreate = new ConcurrentHashMap<>(contentMap.size());
2805+
ConcurrentMap<File, byte[]> filesToReplace = new ConcurrentHashMap<>(contentMap.size());
2806+
int updateFlags = (derived ? IResource.DERIVED : IResource.NONE) | (force ? IResource.FORCE : IResource.NONE)
2807+
| (keepHistory ? IResource.KEEP_HISTORY : IResource.NONE);
2808+
int createFlags = (force ? IResource.FORCE : IResource.NONE) | (derived ? IResource.DERIVED : IResource.NONE);
2809+
SubMonitor subMon = SubMonitor.convert(monitor, contentMap.size());
2810+
for (Entry<IFile, byte[]> e : contentMap.entrySet()) {
2811+
IFile file = Objects.requireNonNull(e.getKey());
2812+
byte[] content = Objects.requireNonNull(e.getValue());
2813+
if (file.exists()) {
2814+
if (file instanceof File f) {
2815+
filesToReplace.put(f, content);
2816+
} else {
2817+
file.setContents(content, updateFlags, subMon.split(1));
2818+
}
2819+
} else {
2820+
if (file instanceof File f) {
2821+
filesToCreate.put(f, content);
2822+
} else {
2823+
file.create(content, createFlags, subMon.split(1));
2824+
}
2825+
}
2826+
}
2827+
for (Entry<File, byte[]> e : filesToReplace.entrySet()) {
2828+
File file = e.getKey();
2829+
byte[] content = e.getValue();
2830+
file.setContents(content, updateFlags, subMon.split(1));
2831+
}
2832+
createMultiple(filesToCreate, createFlags, subMon.split(filesToCreate.size()), executorService);
2833+
}
2834+
2835+
/** @see File#create(byte[], int, IProgressMonitor) **/
2836+
private void createMultiple(ConcurrentMap<File, byte[]> filesToCreate, int updateFlags, IProgressMonitor monitor,
2837+
ExecutorService executorService) throws CoreException {
2838+
if (filesToCreate.isEmpty()) {
2839+
return;
2840+
}
2841+
Set<File> files = filesToCreate.keySet();
2842+
for (File file : files) {
2843+
file.checkValidPath(file.path, IResource.FILE, true);
2844+
}
2845+
2846+
IPath name = files.iterator().next().getFullPath(); // XXX any name
2847+
SubMonitor subMonitor = SubMonitor.convert(monitor, NLS.bind(Messages.resources_creating, name), 1);
2848+
try {
2849+
ISchedulingRule rule = MultiRule
2850+
.combine(files.stream().map(getRuleFactory()::createRule).toArray(ISchedulingRule[]::new));
2851+
NullProgressMonitor npm = new NullProgressMonitor();
2852+
try {
2853+
prepareOperation(rule, npm);
2854+
for (File file : files) {
2855+
file.checkCreatable();
2856+
}
2857+
beginOperation(true);
2858+
try {
2859+
File.internalSetMultipleContents(filesToCreate, updateFlags, false, subMonitor.newChild(1),
2860+
executorService);
2861+
} catch (CoreException | OperationCanceledException e) {
2862+
// CoreException when a problem happened creating a file on disk
2863+
// OperationCanceledException when the operation of setting contents has been
2864+
// canceled
2865+
// In either case delete from the workspace and disk
2866+
for (File file : files) {
2867+
try {
2868+
deleteResource(file);
2869+
IFileStore store = file.getStore();
2870+
store.delete(EFS.NONE, null);
2871+
} catch (Exception e2) {
2872+
e.addSuppressed(e);
2873+
}
2874+
}
2875+
throw e;
2876+
}
2877+
} catch (OperationCanceledException e) {
2878+
getWorkManager().operationCanceled();
2879+
throw e;
2880+
} finally {
2881+
endOperation(rule, true);
2882+
}
2883+
} finally {
2884+
subMonitor.done();
2885+
}
2886+
}
27932887
}

resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IWorkspace.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,20 @@
1818
import java.io.InputStream;
1919
import java.net.URI;
2020
import java.util.Map;
21+
import java.util.Map.Entry;
22+
import java.util.Objects;
23+
import java.util.concurrent.ExecutorService;
2124
import org.eclipse.core.resources.team.FileModificationValidationContext;
22-
import org.eclipse.core.runtime.*;
25+
import org.eclipse.core.runtime.CoreException;
26+
import org.eclipse.core.runtime.IAdaptable;
27+
import org.eclipse.core.runtime.ICoreRunnable;
28+
import org.eclipse.core.runtime.IPath;
29+
import org.eclipse.core.runtime.IProgressMonitor;
30+
import org.eclipse.core.runtime.IStatus;
31+
import org.eclipse.core.runtime.MultiStatus;
32+
import org.eclipse.core.runtime.OperationCanceledException;
33+
import org.eclipse.core.runtime.Plugin;
34+
import org.eclipse.core.runtime.SubMonitor;
2335
import org.eclipse.core.runtime.jobs.ISchedulingRule;
2436

2537
/**
@@ -1810,4 +1822,43 @@ public ProjectOrder(IProject[] projects, boolean hasCycles, IProject[][] knots)
18101822
* @since 2.1
18111823
*/
18121824
IPathVariableManager getPathVariableManager();
1825+
1826+
/**
1827+
* Creates the files and sets/replaces the files content. This is a batch
1828+
* version of {@code IFile.write(...)}. The files are touched in no particuar
1829+
* order and the operation is not guaranteed to be atomic: Exceptions may relate
1830+
* to one or multiple files - some files may have been created and other not.
1831+
* IResourceChangeListener may receive one or multiple events.
1832+
*
1833+
* @param contentMap the new content bytes for each IFile. The map must not
1834+
* be null and must not contain null keys or null values.
1835+
* @param force a flag controlling how to deal with resources that are
1836+
* not in sync with the local file system
1837+
* @param derived Specifying this flag is equivalent to atomically
1838+
* calling {@link IResource#setDerived(boolean)}
1839+
* immediately after creating the resource or atomically
1840+
* setting the derived flag before setting the content of
1841+
* an already existing file if derived==true. A value of
1842+
* false will not update the derived flag of an existing
1843+
* file.
1844+
* @param keepHistory a flag indicating whether or not store the current
1845+
* contents in the local history if the file did already
1846+
* exist
1847+
* @param monitor a progress monitor, or <code>null</code> if progress
1848+
* reporting is not desired
1849+
* @param executorService a ExecutorService to support parallel IO
1850+
* @throws CoreException if this method fails or is canceled.
1851+
* @since 3.22
1852+
* @see IFile#write(byte[], boolean, boolean, boolean, IProgressMonitor)
1853+
*/
1854+
public default void write(Map<IFile, byte[]> contentMap, boolean force, boolean derived, boolean keepHistory,
1855+
IProgressMonitor monitor, ExecutorService executorService) throws CoreException {
1856+
// this code is just meant as an explanation and
1857+
// meant to be overridden with a parallel implementation for local files:
1858+
Objects.requireNonNull(contentMap);
1859+
SubMonitor subMon = SubMonitor.convert(monitor, contentMap.size());
1860+
for (Entry<IFile, byte[]> e : contentMap.entrySet()) {
1861+
e.getKey().write(e.getValue(), force, derived, keepHistory, subMon.split(1));
1862+
}
1863+
}
18131864
}

resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/IFileTest.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@
4444
import java.nio.charset.StandardCharsets;
4545
import java.nio.file.Files;
4646
import java.util.ArrayList;
47+
import java.util.HashMap;
4748
import java.util.List;
49+
import java.util.Map;
50+
import java.util.Map.Entry;
51+
import java.util.concurrent.ExecutorService;
52+
import java.util.concurrent.Executors;
4853
import java.util.concurrent.atomic.AtomicInteger;
4954
import org.eclipse.core.internal.resources.ResourceException;
5055
import org.eclipse.core.resources.IContainer;
@@ -576,6 +581,94 @@ public void testWrite() throws CoreException {
576581
}
577582
}
578583

584+
// @Test // does not test anything but only measures the performance benefit
585+
public void _testWritePerformanceBatch_() throws CoreException {
586+
createInWorkspace(projects[0]);
587+
Map<IFile, byte[]> fileMap2 = new HashMap<>();
588+
Map<IFile, byte[]> fileMap1 = new HashMap<>();
589+
for (int i = 0; i < 1000; i++) {
590+
IFile file = projects[0].getFile("My" + i + ".class");
591+
removeFromWorkspace(file);
592+
((i % 2 == 0) ? fileMap1 : fileMap2).put(file, ("smallFileContent" + i).getBytes());
593+
}
594+
{
595+
long n0 = System.nanoTime();
596+
ExecutorService executorService = Executors.newWorkStealingPool();
597+
ResourcesPlugin.getWorkspace().write(fileMap1, false, true, false, null, executorService);
598+
executorService.shutdownNow();
599+
long n1 = System.nanoTime();
600+
System.out.println("parallel write took:" + (n1 - n0) / 1_000_000 + "ms"); // ~ 250ms with 6 cores
601+
}
602+
{
603+
long n0 = System.nanoTime();
604+
for (Entry<IFile, byte[]> e : fileMap2.entrySet()) {
605+
e.getKey().write(e.getValue(), false, true, false, null);
606+
}
607+
long n1 = System.nanoTime();
608+
System.out.println("sequential write took:" + (n1 - n0) / 1_000_000 + "ms"); // ~ 1500ms
609+
}
610+
}
611+
612+
@Test
613+
public void testWrites() throws CoreException {
614+
ExecutorService executorService = Executors.newWorkStealingPool();
615+
IWorkspaceDescription description = getWorkspace().getDescription();
616+
description.setMaxFileStates(4);
617+
getWorkspace().setDescription(description);
618+
619+
IFile derived = projects[0].getFile("derived.txt");
620+
IFile anyOther = projects[0].getFile("anyOther.txt");
621+
createInWorkspace(projects[0]);
622+
removeFromWorkspace(derived);
623+
removeFromWorkspace(anyOther);
624+
for (int i = 0; i < 16; i++) {
625+
boolean setDerived = i % 2 == 0;
626+
boolean deleteBefore = (i >> 1) % 2 == 0;
627+
boolean keepHistory = (i >> 2) % 2 == 0;
628+
boolean oldDerived1 = false;
629+
if (deleteBefore) {
630+
derived.delete(false, null);
631+
anyOther.delete(false, null);
632+
} else {
633+
oldDerived1 = derived.isDerived();
634+
}
635+
assertEquals(!deleteBefore, derived.exists());
636+
FussyProgressMonitor monitor = new FussyProgressMonitor();
637+
AtomicInteger changeCount = new AtomicInteger();
638+
ResourcesPlugin.getWorkspace().addResourceChangeListener(event -> changeCount.incrementAndGet());
639+
String derivedContent = "updateOrCreate" + i;
640+
String otherContent = "other" + i;
641+
ResourcesPlugin.getWorkspace().write(
642+
Map.of(derived, derivedContent.getBytes(), anyOther, otherContent.getBytes()), false, setDerived,
643+
keepHistory, monitor, executorService);
644+
assertEquals(derivedContent, new String(derived.readAllBytes()));
645+
assertEquals(otherContent, new String(anyOther.readAllBytes()));
646+
monitor.assertUsedUp();
647+
if (deleteBefore) {
648+
assertEquals(setDerived, derived.isDerived());
649+
} else {
650+
assertEquals(oldDerived1 || setDerived, derived.isDerived());
651+
}
652+
assertFalse(derived.isTeamPrivateMember());
653+
assertTrue(derived.exists());
654+
655+
IFileState[] history1 = derived.getHistory(null);
656+
changeCount.set(0);
657+
derivedContent = "update" + i;
658+
otherContent = "dude" + i;
659+
ResourcesPlugin.getWorkspace().write(
660+
Map.of(derived, derivedContent.getBytes(), anyOther, otherContent.getBytes()), false, false,
661+
keepHistory,
662+
null, executorService);
663+
assertEquals(derivedContent, new String(derived.readAllBytes()));
664+
assertEquals(otherContent, new String(anyOther.readAllBytes()));
665+
boolean oldDerived2 = derived.isDerived();
666+
assertEquals(oldDerived2, derived.isDerived());
667+
IFileState[] history2 = derived.getHistory(null);
668+
assertEquals((keepHistory && !oldDerived2) ? 1 : 0, history2.length - history1.length);
669+
}
670+
executorService.shutdown();
671+
}
579672
@Test
580673
public void testWriteRule() throws CoreException {
581674
IFile resource = projects[0].getFile("derived.txt");

0 commit comments

Comments
 (0)