Skip to content

Commit 12d24b5

Browse files
li-feng-scbot-snapci
authored andcommitted
Internal Change
GitOrigin-RevId: d4291cc5bb9b084c10ca89b61011fbb88f6a1170
1 parent 236db72 commit 12d24b5

8 files changed

Lines changed: 521 additions & 98 deletions

valdi/src/java/com/snap/valdi/schema/ValdiMarshallableObjectDescriptor.kt

Lines changed: 175 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ package com.snap.valdi.schema
33
import androidx.annotation.Keep
44
import com.snap.valdi.exceptions.ValdiFatalException
55
import com.snap.valdi.utils.arrayMap
6+
import java.io.ByteArrayOutputStream
67
import java.lang.Exception
78
import java.lang.reflect.Method
9+
import java.nio.ByteBuffer
10+
import java.nio.ByteOrder
11+
import java.nio.charset.StandardCharsets
812
import kotlin.reflect.KClass
913

1014
@Keep
@@ -106,52 +110,188 @@ private constructor(val type: Int,
106110
@JvmStatic
107111
fun getDescriptorForClass(cls: Class<*>): ValdiMarshallableObjectDescriptor {
108112
try {
109-
if (cls.isInterface) {
110-
val valdiInterface = cls.getAnnotation(ValdiInterface::class.java)
111-
if (valdiInterface != null) {
112-
return forInterface(
113-
valdiInterface.schema,
114-
valdiInterface.proxyClass.java,
115-
valdiInterface.typeReferences,
116-
valdiInterface.propertyReplacements)
117-
}
113+
return descriptorFromAnnotations(cls)
114+
} catch (exc: Throwable) {
115+
ValdiFatalException.handleFatal(exc, "Could not resolve descriptor for class ${cls.name}")
116+
}
117+
}
118118

119-
val valdiUntyped = cls.getAnnotation(ValdiUntypedClass::class.java)
120-
if (valdiUntyped != null) {
121-
return forUntyped()
122-
}
119+
@JvmStatic
120+
private fun descriptorFromAnnotations(cls: Class<*>): ValdiMarshallableObjectDescriptor {
121+
if (cls.isInterface) {
122+
val valdiInterface = cls.getAnnotation(ValdiInterface::class.java)
123+
if (valdiInterface != null) {
124+
return forInterface(
125+
valdiInterface.schema,
126+
valdiInterface.proxyClass.java,
127+
valdiInterface.typeReferences,
128+
valdiInterface.propertyReplacements)
123129
}
124130

125-
if (cls.isEnum) {
126-
val valdiEnum = cls.getAnnotation(ValdiEnum::class.java)
127-
if (valdiEnum != null) {
128-
return when (valdiEnum.type) {
129-
ValdiEnumType.INT -> forIntEnum(valdiEnum.schema, valdiEnum.propertyReplacements)
130-
ValdiEnumType.STRING -> forStringEnum(valdiEnum.schema, valdiEnum.propertyReplacements)
131-
}
131+
val valdiUntyped = cls.getAnnotation(ValdiUntypedClass::class.java)
132+
if (valdiUntyped != null) {
133+
return forUntyped()
134+
}
135+
}
136+
137+
if (cls.isEnum) {
138+
val valdiEnum = cls.getAnnotation(ValdiEnum::class.java)
139+
if (valdiEnum != null) {
140+
return when (valdiEnum.type) {
141+
ValdiEnumType.INT -> forIntEnum(valdiEnum.schema, valdiEnum.propertyReplacements)
142+
ValdiEnumType.STRING -> forStringEnum(valdiEnum.schema, valdiEnum.propertyReplacements)
132143
}
133144
}
145+
}
146+
147+
val valdiClass = cls.getAnnotation(ValdiClass::class.java)
148+
if (valdiClass != null) {
149+
return forClass(
150+
valdiClass.schema,
151+
valdiClass.typeReferences,
152+
valdiClass.propertyReplacements)
153+
}
154+
155+
val valdiFunction = cls.getAnnotation(ValdiFunctionClass::class.java)
156+
if (valdiFunction != null) {
157+
return forFunction(
158+
valdiFunction.schema,
159+
valdiFunction.typeReferences,
160+
valdiFunction.propertyReplacements)
161+
}
162+
163+
throw Exception("Could not resolve Valdi Annotation")
164+
}
165+
166+
// --- Batched descriptor fetch (JNI-overhead reduction). See valdi-jni-batch-plan.md ---
167+
168+
/** Binary format version of the buffer produced by [getDescriptorClosure]. Must match the C++
169+
* parser in AndroidValueMarshallerRegistry. Bump on any layout change. */
170+
const val DESCRIPTOR_CLOSURE_FORMAT_VERSION: Int = 2
134171

135-
val valdiClass = cls.getAnnotation(ValdiClass::class.java)
136-
if (valdiClass != null) {
137-
return forClass(
138-
valdiClass.schema,
139-
valdiClass.typeReferences,
140-
valdiClass.propertyReplacements)
172+
/**
173+
* Type-reference classes of [cls], read from its Valdi annotation. The annotation stores them
174+
* as already-resolved [KClass] (not names), so traversal needs no name→Class lookup and is
175+
* classloader-safe. Enums and untyped types have none.
176+
*/
177+
@JvmStatic
178+
private fun referencedClasses(cls: Class<*>): Array<KClass<*>> {
179+
return cls.getAnnotation(ValdiClass::class.java)?.typeReferences
180+
?: cls.getAnnotation(ValdiInterface::class.java)?.typeReferences
181+
?: cls.getAnnotation(ValdiFunctionClass::class.java)?.typeReferences
182+
?: emptyArray()
183+
}
184+
185+
/**
186+
* Whether [cls] is a Valdi marshallable type (carries one of the Valdi schema annotations).
187+
* Type references can point at non-marshallable classes (e.g. java.lang.Object from an untyped
188+
* field); those must be skipped by the closure walk — [getDescriptorForClass] would fatal on
189+
* them, and the registry resolves them through its own path instead.
190+
*/
191+
@JvmStatic
192+
private fun hasValdiDescriptor(cls: Class<*>): Boolean {
193+
return cls.isAnnotationPresent(ValdiClass::class.java) ||
194+
cls.isAnnotationPresent(ValdiInterface::class.java) ||
195+
cls.isAnnotationPresent(ValdiFunctionClass::class.java) ||
196+
cls.isAnnotationPresent(ValdiEnum::class.java) ||
197+
cls.isAnnotationPresent(ValdiUntypedClass::class.java)
198+
}
199+
200+
private fun writeLengthPrefixed(out: ByteArrayOutputStream, value: String) {
201+
// Payloads are ASCII (schema syntax + JVM class names), so ISO-8859-1 is a 1:1 byte copy.
202+
// Length is u32: a large @ExportModel's schema can exceed 64KB, and a u16 prefix would
203+
// silently truncate it (size and 0xFFFF) and desync the rest of the buffer.
204+
val bytes = value.toByteArray(StandardCharsets.ISO_8859_1)
205+
out.write(bytes.size and 0xFF)
206+
out.write((bytes.size ushr 8) and 0xFF)
207+
out.write((bytes.size ushr 16) and 0xFF)
208+
out.write((bytes.size ushr 24) and 0xFF)
209+
out.write(bytes)
210+
}
211+
212+
/**
213+
* JVM class names already visited by a prior [getDescriptorClosure] call (every class packed
214+
* into a buffer, plus non-marshallable references that were skipped). Accumulated across calls
215+
* and reused as the walk's `visited` set so each call prunes to the not-yet-resolved frontier,
216+
* skipping already-cached classes and their subtrees.
217+
*
218+
* This mirrors the C++ registry's descriptor cache without C++ echoing it back each call: that
219+
* cache belongs to a process-singleton ([ValdiValueMarshallerRegistry.shared]), is append-only,
220+
* and is never cleared, so the two stay in sync by construction. Every call originates from the
221+
* C++ registry under its schema-registry lock, so access here is already serialized — no extra
222+
* synchronization needed. (If the two ever diverged, e.g. C++ dropped a buffer, the registry
223+
* self-heals via its legacy per-class fallback.)
224+
*/
225+
private val resolvedClassNames = HashSet<String>()
226+
227+
/**
228+
* Resolve [root] and its transitive type-reference closure, returning every not-yet-resolved
229+
* descriptor packed into one direct [ByteBuffer] for zero-copy parsing in C++. Replaces N
230+
* per-class getDescriptorForClass JNI round-trips (+ per-field/array read-backs) with a single
231+
* batched fetch; C++ caches the parsed descriptors by name so reference recursion becomes cache
232+
* hits. Classes packed by a prior call are tracked in [resolvedClassNames] and skipped (with
233+
* their subtrees), so each call costs only the new frontier, not the whole accumulated cache.
234+
*
235+
* Layout (little-endian, ASCII payloads):
236+
* [u8 version][u32 count] then, per entry:
237+
* [u32+className][u8 type][u32+schema][u32+propertyReplacements][u32+proxyClassName]
238+
* [u16 refCount]{ [u32+refName] }
239+
*/
240+
@Keep
241+
@JvmStatic
242+
fun getDescriptorClosure(root: Class<*>): ByteBuffer {
243+
// Reuse the running set directly as `visited`: classes resolved by earlier calls (and their
244+
// subtrees) are skipped, so the walk only touches the new frontier. Serialized by the C++
245+
// registry lock (see [resolvedClassNames]).
246+
val visited = resolvedClassNames
247+
val queue = ArrayDeque<Class<*>>()
248+
queue.addLast(root)
249+
250+
val body = ByteArrayOutputStream(8192)
251+
var count = 0
252+
253+
while (queue.isNotEmpty()) {
254+
// Use removeAt(0): the remove-first extension resolves to the Java 21
255+
// SequencedCollection method and NoSuchMethodErrors on older Android runtimes
256+
// (banned by check_unsupported_android_15_method_usage).
257+
val cls = queue.removeAt(0)
258+
if (!visited.add(cls.name)) {
259+
continue
141260
}
261+
if (!hasValdiDescriptor(cls)) {
262+
// Non-marshallable reference (e.g. java.lang.Object from an untyped field). Don't
263+
// fetch (getDescriptorForClass would fatal) or recurse; the registry resolves such
264+
// references via its own path, falling back to legacy per-class resolution.
265+
continue
266+
}
267+
268+
val descriptor = getDescriptorForClass(cls)
269+
writeLengthPrefixed(body, cls.name)
270+
body.write(descriptor.type and 0xFF)
271+
writeLengthPrefixed(body, descriptor.schema)
272+
writeLengthPrefixed(body, descriptor.propertyReplacements ?: "")
273+
writeLengthPrefixed(body, descriptor.proxyClass?.name ?: "")
142274

143-
val valdiFunction = cls.getAnnotation(ValdiFunctionClass::class.java)
144-
if (valdiFunction != null) {
145-
return forFunction(
146-
valdiFunction.schema,
147-
valdiFunction.typeReferences,
148-
valdiFunction.propertyReplacements)
275+
val refNames = descriptor.typeReferences ?: emptyArray()
276+
body.write(refNames.size and 0xFF)
277+
body.write((refNames.size ushr 8) and 0xFF)
278+
for (refName in refNames) {
279+
writeLengthPrefixed(body, refName)
149280
}
281+
count++
150282

151-
throw Exception("Could not resolve Valdi Annotation")
152-
} catch (exc: Throwable) {
153-
ValdiFatalException.handleFatal(exc, "Could not resolve descriptor for class ${cls.name}")
283+
for (ref in referencedClasses(cls)) {
284+
queue.addLast(ref.java)
285+
}
154286
}
287+
288+
val bodyBytes = body.toByteArray()
289+
val buffer = ByteBuffer.allocateDirect(1 + 4 + bodyBytes.size).order(ByteOrder.LITTLE_ENDIAN)
290+
buffer.put(DESCRIPTOR_CLOSURE_FORMAT_VERSION.toByte())
291+
buffer.putInt(count)
292+
buffer.put(bodyBytes)
293+
buffer.flip()
294+
return buffer
155295
}
156296

157297
@JvmStatic

valdi/src/java/com/snap/valdi/schema/ValdiValueMarshallerRegistry.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ interface ValdiValueMarshallerRegistry {
1818

1919
fun disposeObject(cls: Class<*>, obj: Any)
2020

21+
/**
22+
* Enables the batched descriptor-closure fast path in the native registry (default off). Gated by
23+
* a COF and set once at startup; a no-op for backends that don't implement the optimization.
24+
*/
25+
fun setDescriptorClosureEnabled(enabled: Boolean) {}
26+
2127

2228
companion object {
2329
@JvmStatic

valdi/src/java/com/snap/valdi/schema/ValdiValueMarshallerRegistryCpp.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ class ValdiValueMarshallerRegistryCpp: NativeHandleWrapper(nativeCreate()), Vald
1414
nativeDestroy(handle)
1515
}
1616

17+
override fun setDescriptorClosureEnabled(enabled: Boolean) {
18+
nativeSetDescriptorClosureEnabled(nativeHandle, enabled)
19+
}
20+
1721
override fun marshallObject(cls: Class<*>, marshaller: ValdiMarshaller, obj: Any): Int {
1822
return nativeMarshallObject(nativeHandle, cls.name, marshaller.nativeHandle, obj)
1923
}
@@ -67,6 +71,8 @@ class ValdiValueMarshallerRegistryCpp: NativeHandleWrapper(nativeCreate()), Vald
6771
@JvmStatic
6872
private external fun nativeDestroy(ptr: Long)
6973
@JvmStatic
74+
private external fun nativeSetDescriptorClosureEnabled(ptr: Long, enabled: Boolean)
75+
@JvmStatic
7076
private external fun nativeMarshallObject(ptr: Long,
7177
className: String,
7278
marshallerHandle: Long,

0 commit comments

Comments
 (0)