Skip to content

Commit 5576979

Browse files
haxorzcopybara-github
authored andcommitted
Have Dict#contents switch to an ImmutableMap when the Dict is frozen.
This implements the idea I documented in a TODO in 24ec910. The approach is to introduce an optional mechanism for Starlark values to get notified when their `Mutability` gets frozen, and to use that mechanism in `Dict`. In implementing this idea, I noticed yet another idea for saving memory. I added a TODO for that, and I also added a TODO for a code unification noticed by arostovtsev@ during code review. For a `bazel build --nobuild //very/large:target` invocation with many retained `Dict` instances, this approach reduces retained heap by ~1%. There is a ~0.65% increase in CPU usage but no measurable increase in wall time, so I think this tradeoff is very much so worth it. PiperOrigin-RevId: 433563032
1 parent f815bf0 commit 5576979

File tree

4 files changed

+155
-8
lines changed

4 files changed

+155
-8
lines changed

src/main/java/net/starlark/java/eval/Dict.java

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package net.starlark.java.eval;
1616

17+
import com.google.common.annotations.VisibleForTesting;
1718
import com.google.common.base.Preconditions;
1819
import com.google.common.collect.ImmutableMap;
1920
import com.google.common.collect.ImmutableSet;
@@ -111,8 +112,21 @@ public class Dict<K, V>
111112
StarlarkIndexable,
112113
StarlarkIterable<K> {
113114

114-
private final Map<K, V> contents;
115-
private int iteratorCount; // number of active iterators (unused once frozen)
115+
/**
116+
* The contents of the Dict.
117+
*
118+
* <p>This will either be a {@link LinkedHashMap} (for a mutable Dict that hasn't yet been frozen)
119+
* or a {@link ImmutableMap} (for a Dict that started frozen or has been frozen; see {@link
120+
* #Dict(ImmutableMap)} and {@link #onFreeze}, respectively).
121+
*/
122+
private Map<K, V> contents;
123+
124+
/**
125+
* The number of active iterators.
126+
*
127+
* <p>This is unused after the Dict is frozen.
128+
*/
129+
private int iteratorCount;
116130

117131
/** Final except for {@link #unsafeShallowFreeze}; must not be modified any other way. */
118132
private Mutability mutability;
@@ -121,8 +135,7 @@ private Dict(Mutability mutability, LinkedHashMap<K, V> contents) {
121135
Preconditions.checkNotNull(mutability);
122136
Preconditions.checkState(mutability != Mutability.IMMUTABLE);
123137
this.mutability = mutability;
124-
// TODO(bazel-team): Memory optimization opportunity: Make it so that a call to
125-
// `mutability.freeze()` causes `contents` here to become an ImmutableMap.
138+
mutability.notifyOnFreeze(this);
126139
this.contents = contents;
127140
}
128141

@@ -560,6 +573,35 @@ public Mutability mutability() {
560573
public void unsafeShallowFreeze() {
561574
Mutability.Freezable.checkUnsafeShallowFreezePrecondition(this);
562575
this.mutability = Mutability.IMMUTABLE;
576+
this.contents = ImmutableMap.copyOf(contents);
577+
}
578+
579+
/**
580+
* Switches {@link #contents} to be a {@link ImmutableMap} in order to save memory.
581+
*
582+
* <p>See the comment in {@link #Dict(ImmutableMap)} for details.
583+
*/
584+
@Override
585+
public void onFreeze() {
586+
if (contents instanceof LinkedHashMap) {
587+
this.contents = ImmutableMap.copyOf(contents);
588+
// TODO(bazel-team): Make #mutability IMMUTABLE, causing the old Mutability instance to be
589+
// eligible for GC. Do this in StarlarkList too.
590+
} else {
591+
// Assert #onFreeze hasn't been called before. But also hackily support the usage in
592+
// ScriptTest.
593+
//
594+
// ScriptTest extends the language with a `freeze` feature, allowing #unsafeShallowFreeze to
595+
// be called. When the bzl code has already caused that method to be called, #contents will
596+
// already be an ImmutableMap, but #mutability will still not be.
597+
//
598+
// And we definitely want #unsafeShallowFreeze to cause #contents to become an ImmutableMap.
599+
// That method is used at the end of deserialization, which uses a mutable #mutability that
600+
// became IMMUTABLE in that method.
601+
//
602+
// TODO(bazel-team): Combine the two methods.
603+
Preconditions.checkState(mutability == Mutability.IMMUTABLE);
604+
}
563605
}
564606

565607
/**
@@ -623,6 +665,11 @@ public String toString() {
623665
return Starlark.repr(this);
624666
}
625667

668+
@VisibleForTesting
669+
Map<K, V> getContentsForTesting() {
670+
return contents;
671+
}
672+
626673
/**
627674
* Casts a non-null Starlark value {@code x} to a {@code Dict<K, V>} after checking that all keys
628675
* and values are instances of {@code keyType} and {@code valueType}, respectively. On error, it

src/main/java/net/starlark/java/eval/Mutability.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package net.starlark.java.eval;
1515

1616
import com.google.common.base.Joiner;
17+
import java.util.ArrayList;
1718
import java.util.IdentityHashMap;
1819

1920
/**
@@ -105,6 +106,8 @@ public final class Mutability implements AutoCloseable {
105106
/** Controls access to {@link Freezable#unsafeShallowFreeze}. */
106107
private final boolean allowsUnsafeShallowFreeze;
107108

109+
private ArrayList<Freezable> freezablesToNotifyOnFreeze;
110+
108111
private Mutability(Object[] annotation, boolean allowsUnsafeShallowFreeze) {
109112
this.annotation = annotation;
110113
this.allowsUnsafeShallowFreeze = allowsUnsafeShallowFreeze;
@@ -171,13 +174,22 @@ private boolean updateIteratorCount(Freezable x, int delta) {
171174
* Freezes this {@code Mutability}, rendering all {@link Freezable} objects that refer to it
172175
* immutable.
173176
*
174-
* Note that freezing does not directly touch all the {@code Freezables}, so this operation is
175-
* constant-time.
177+
* <p>Note that freezing directly touches only the {@code Freezables} for which there was a call
178+
* to {@code thisMutability.notifyOnFreeze(thatFreezable)}, so this is linear-time in the number
179+
* of such {@code Freezables}s.
176180
*
177181
* @return this object, in the fluent style
178182
*/
179183
public Mutability freeze() {
180184
this.iteratorCount = null;
185+
186+
if (freezablesToNotifyOnFreeze != null) {
187+
for (Freezable freezable : freezablesToNotifyOnFreeze) {
188+
freezable.onFreeze();
189+
}
190+
freezablesToNotifyOnFreeze = null;
191+
}
192+
181193
return this;
182194
}
183195

@@ -188,12 +200,22 @@ public void close() {
188200

189201
/**
190202
* Returns whether {@link Freezable}s having this {@code Mutability} allow the {@link
191-
* #unsafeShallowFreeze} operation.
203+
* Freezable#unsafeShallowFreeze} operation.
192204
*/
193205
public boolean allowsUnsafeShallowFreeze() {
194206
return allowsUnsafeShallowFreeze;
195207
}
196208

209+
/**
210+
* Causes {@code freezable.onFreeze()} to be called in the future when {@link #freeze} is called.
211+
*/
212+
public void notifyOnFreeze(Freezable freezable) {
213+
if (freezablesToNotifyOnFreeze == null) {
214+
freezablesToNotifyOnFreeze = new ArrayList<>();
215+
}
216+
freezablesToNotifyOnFreeze.add(freezable);
217+
}
218+
197219
/**
198220
* An object that refers to a {@link Mutability} to decide whether to allow mutation. All {@link
199221
* Freezable} Starlark objects created within a given {@link StarlarkThread} will share the same
@@ -207,6 +229,14 @@ public interface Freezable {
207229
*/
208230
Mutability mutability();
209231

232+
/**
233+
* If {@code mutability().notifyOnFreeze(this)} has been called, this method gets called when
234+
* {@code mutability().freeze()} gets called.
235+
*
236+
* <p>Do not call this method from outside {@link Mutability#freeze}.
237+
*/
238+
default void onFreeze() {}
239+
210240
/**
211241
* Registers a change to this Freezable's iterator count and reports whether it is temporarily
212242
* immutable.

src/test/java/net/starlark/java/eval/MutabilityTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,44 @@ public void checkUnsafeShallowFreezePrecondition_SucceedsWhenAllowed() throws Ex
127127
Mutability mutability = Mutability.createAllowingShallowFreeze("test");
128128
Freezable.checkUnsafeShallowFreezePrecondition(new DummyFreezable(mutability));
129129
}
130+
131+
@Test
132+
public void notifyOnFreeze() {
133+
class FreezableWithNotify implements Freezable {
134+
private final Mutability mutability;
135+
private boolean onFreezeCalled = false;
136+
137+
FreezableWithNotify(Mutability mutability) {
138+
this.mutability = mutability;
139+
}
140+
141+
@Override
142+
public Mutability mutability() {
143+
return mutability;
144+
}
145+
146+
@Override
147+
public void onFreeze() {
148+
onFreezeCalled = true;
149+
}
150+
}
151+
152+
// When we have an unfrozen Mutability,
153+
Mutability mutability = Mutability.create("test");
154+
155+
// And we create two Freezables using the Mutability,
156+
FreezableWithNotify freezable1 = new FreezableWithNotify(mutability);
157+
FreezableWithNotify freezable2 = new FreezableWithNotify(mutability);
158+
159+
// And we tell the Mutability to notify one of the two Freezables when it has been frozen,
160+
mutability.notifyOnFreeze(freezable1);
161+
162+
// And we freeze the Mutability,
163+
mutability.freeze();
164+
165+
// Then the Freezable that was supposed to be notified was notified,
166+
assertThat(freezable1.onFreezeCalled).isTrue();
167+
// And the other one wasn't.
168+
assertThat(freezable2.onFreezeCalled).isFalse();
169+
}
130170
}

src/test/java/net/starlark/java/eval/StarlarkMutableTest.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@
2020
import com.google.common.collect.ImmutableList;
2121
import com.google.common.collect.ImmutableMap;
2222
import java.util.Iterator;
23+
import java.util.LinkedHashMap;
2324
import java.util.List;
2425
import java.util.Map;
2526
import org.junit.Test;
2627
import org.junit.runner.RunWith;
2728
import org.junit.runners.JUnit4;
2829

29-
/** Tests for {@link StarlarkMutable}. */
30+
/** Tests for how Starlark value implementations interact with {@link Mutability#freeze}. */
3031
@RunWith(JUnit4.class)
3132
public final class StarlarkMutableTest {
3233

@@ -142,4 +143,33 @@ public void testDictBuilder() throws Exception {
142143
.isEqualTo(
143144
"{\"one\": \"1\", \"two\": \"22\", \"three\": \"33\", \"four\": \"4\"}"); // unchanged
144145
}
146+
147+
@Test
148+
public void testImmutableDictUsesImmutableMap() {
149+
Mutability mu = Mutability.IMMUTABLE;
150+
Dict<String, String> dict = Dict.<String, String>builder().put("cat", "dog").build(mu);
151+
assertThat(dict.getContentsForTesting()).isInstanceOf(ImmutableMap.class);
152+
assertThat(dict.getContentsForTesting()).containsExactly("cat", "dog");
153+
}
154+
155+
@Test
156+
public void testMutableDictSwitchesToImmutableMapWhenFrozen() {
157+
Mutability mu = Mutability.create("test");
158+
Dict<String, String> dict = Dict.<String, String>builder().put("cat", "dog").build(mu);
159+
assertThat(dict.getContentsForTesting()).isInstanceOf(LinkedHashMap.class);
160+
assertThat(dict.getContentsForTesting()).containsExactly("cat", "dog");
161+
mu.freeze();
162+
assertThat(dict.getContentsForTesting()).isInstanceOf(ImmutableMap.class);
163+
assertThat(dict.getContentsForTesting()).containsExactly("cat", "dog");
164+
}
165+
166+
@Test
167+
public void testMutableDictSwitchesToImmutableMapWhenFrozen_usesCanonicalEmptyImmutableMap() {
168+
Mutability mu = Mutability.create("test");
169+
Dict<String, String> dict = Dict.<String, String>builder().build(mu);
170+
assertThat(dict.getContentsForTesting()).isInstanceOf(LinkedHashMap.class);
171+
assertThat(dict.getContentsForTesting()).isEmpty();
172+
mu.freeze();
173+
assertThat(dict.getContentsForTesting()).isSameInstanceAs(ImmutableMap.of());
174+
}
145175
}

0 commit comments

Comments
 (0)