Initial API

This commit is contained in:
2025-04-21 11:27:13 +02:00
commit 5f91256c31
23 changed files with 1317 additions and 0 deletions

View File

@ -0,0 +1,15 @@
package com.jaytux.simd
import io.github.cdimascio.dotenv.dotenv
object DotEnv {
val env = dotenv()
operator fun get(name: String) = env[name]
class DotEnvException(missing: String) : RuntimeException("Missing environment variable: $missing") {
constructor(missing: String, cause: Throwable) : this(missing) {
initCause(cause)
}
}
}

View File

@ -0,0 +1,55 @@
package com.jaytux.simd
import com.jaytux.simd.data.Database
import com.jaytux.simd.data.Loader
import com.jaytux.simd.server.configureHTTP
import com.jaytux.simd.server.configureRouting
import com.jaytux.simd.server.configureSerialization
import io.ktor.server.application.*
import io.ktor.server.netty.*
import kotlinx.coroutines.runBlocking
fun main(args: Array<String>) {
if(args.size > 3 && args[1] == "-reload") {
val xmlFile = args[2]
val jsonFile = args[3]
dbSetup(xmlFile, jsonFile)
}
else if(args.size >= 1 && args[1] == "-h") {
println("Usage: ${args[0]} -reload <xmlFile> <jsonFile> (to reload the database)")
println(" ${args[0]} (to start the server)")
}
else {
EngineMain.main(args)
}
}
fun dbSetup(xmlFile: String, jsonFile: String) {
runBlocking {
val xml = Loader.loadXml("/home/jay/intrinsics/data.xml")
val perf = Loader.loadJson("/home/jay/intrinsics/perf2.js")
Loader.importToDb(xml, perf)
}
}
fun Application.module() {
Database.db
configureSerialization()
configureHTTP()
configureRouting()
}
// API: (everything except /details/ is paginated per 100)
// - GET /all (list of SIMD intrinsics (name + ID))
// - GET /cpuid (list of CPUID values)
// - GET /tech (list of techs)
// - GET /category (list of categories)
// - GET /types (list of types)
// - GET /search (search for SIMD intrinsics); query params:
// - name (string, optional, partial matching)
// - return (string, optional, full match)
// - cpuid (string, optional, full match)
// - tech (string, optional, full match)
// - category (string, optional, full match)
// - desc (string, optional, partial matching)
// - GET /details/<id> (details of a SIMD intrinsic)

View File

@ -0,0 +1,32 @@
package com.jaytux.simd.data
import com.jaytux.simd.DotEnv
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
object Database {
val db by lazy {
val db = Database.connect(
url = DotEnv["DATABASE_URL"] ?: throw DotEnv.DotEnvException("DATABASE_URL"),
driver = DotEnv["DATABASE_DRIVER"] ?: throw DotEnv.DotEnvException("DATABASE_DRIVER"),
user = DotEnv["DATABASE_USER"] ?: "",
password = DotEnv["DATABASE_PASSWORD"] ?: "",
)
transaction {
SchemaUtils.create(Techs, CppTypes, Categories, CPUIDs, Intrinsics,
IntrinsicArguments, IntrinsicInstructions, Platforms, Performances)
}
db
}
fun reset() {
transaction {
SchemaUtils.drop(Techs, CppTypes, Categories, CPUIDs, Intrinsics,
IntrinsicArguments, IntrinsicInstructions, Platforms, Performances)
SchemaUtils.create(Techs, CppTypes, Categories, CPUIDs, Intrinsics,
IntrinsicArguments, IntrinsicInstructions, Platforms, Performances)
}
}
}

View File

@ -0,0 +1,82 @@
package com.jaytux.simd.data
import com.jaytux.simd.data.CppType.Companion.optionalReferrersOn
import org.jetbrains.exposed.dao.*
import org.jetbrains.exposed.dao.id.CompositeID
import org.jetbrains.exposed.dao.id.EntityID
import java.util.*
class Tech(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<Tech>(Techs)
var name by Techs.name
}
class CppType(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<CppType>(CppTypes)
var name by CppTypes.name
}
class Category(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<Category>(Categories)
var name by Categories.name
}
class CPUID(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<CPUID>(CPUIDs)
var name by CPUIDs.name
}
class Intrinsic(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<Intrinsic>(Intrinsics)
var mnemonic by Intrinsics.mnemonic
var returnType by CppType referencedOn Intrinsics.returnType
var returnVar by Intrinsics.returnVar
var description by Intrinsics.description
var operations by Intrinsics.operations
var category by Category referencedOn Intrinsics.category
var cpuid by CPUID optionalReferencedOn Intrinsics.cpuid
var tech by Tech referencedOn Intrinsics.tech
val arguments by IntrinsicArgument referrersOn IntrinsicArguments.intrinsic
val instructions by IntrinsicInstruction referrersOn IntrinsicInstructions.intrinsic
}
class IntrinsicArgument(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<IntrinsicArgument>(IntrinsicArguments)
var intrinsic by Intrinsic referencedOn IntrinsicArguments.intrinsic
var name by IntrinsicArguments.name
var type by CppType referencedOn IntrinsicArguments.type
var index by IntrinsicArguments.index
}
class IntrinsicInstruction(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<IntrinsicInstruction>(IntrinsicInstructions)
var intrinsic by Intrinsic referencedOn IntrinsicInstructions.intrinsic
var mnemonic by IntrinsicInstructions.mnemonic
var xed by IntrinsicInstructions.xed
var form by IntrinsicInstructions.form
val performance by Performance referrersOn Performances.instruction
}
class Platform(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<Platform>(Platforms)
var name by Platforms.name
}
class Performance(id: EntityID<CompositeID>) : CompositeEntity(id) {
companion object : CompositeEntityClass<Performance>(Performances)
var instruction by IntrinsicInstruction referencedOn Performances.instruction
var platform by Platform referencedOn Performances.platform
var latency by Performances.latency
var throughput by Performances.throughput
}

View File

@ -0,0 +1,205 @@
package com.jaytux.simd.data
import com.fleeksoft.ksoup.Ksoup
import com.jaytux.simd.data.IntrinsicInstructions.xed
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.transactions.transaction
import java.io.File
import org.json.JSONObject
object Loader {
data class XmlIntrinsic(val name: String, val tech: String, val retType: String, val retVar: String?, val args: List<Pair<String, String>>, val desc: String, val op: String?, val insn: List<Triple<String, String, String?>>, val cpuid: String?, val category: String)
data class XmlData(
val types: Set<String>, val techs: Set<String>, val cpuids: Set<String>, val categories: Set<String>,
val intrinsics: List<XmlIntrinsic>
)
data class Performance(val latency: Float?, val throughput: Float?)
data class JsonData(val platforms: Set<String>, val data: Map<String, Map<String, Performance>>)
suspend fun loadXml(xmlFile: String): XmlData = coroutineScope {
val xml = Ksoup.parseXml(File(xmlFile).readText(Charsets.UTF_8))
val cppTypes = mutableSetOf<String>()
val techs = mutableSetOf<String>()
val cpuids = mutableSetOf<String>()
val categories = mutableSetOf<String>()
val intrins = mutableListOf<XmlIntrinsic>()
val errors = mutableListOf<String>()
xml.getElementsByTag("intrinsic").forEachIndexed { i, it ->
val name = it.attribute("name")?.value
if(name == null) {
errors += "Missing name attribute in intrinsic element"
return@forEachIndexed
}
val tech = it.attribute("tech")?.value
if(tech == null) {
errors += "Missing tech attribute for intrinsic $name"
return@forEachIndexed
}
val ret = it.getElementsByTag("return").firstOrNull()
if(ret == null) {
errors += "Missing return element for intrinsic $name"
return@forEachIndexed
}
val retType = ret.attribute("type")?.value
if(retType == null) {
errors += "Missing type attribute for return element in intrinsic $name"
return@forEachIndexed
}
val retVar = ret.attribute("varname")?.value
val args = mutableListOf<Pair<String, String>>()
it.getElementsByTag("parameter").forEachIndexed { i, p ->
val argName = p.attribute("varname")?.value
val type = p.attribute("type")?.value
if(type != null && type == "void") return@forEachIndexed //ignore
if(argName == null) {
errors += "Missing varname attribute for parameter $i in intrinsic $name"
return@forEachIndexed
}
if(type == null) {
errors += "Missing type attribute for parameter $argName in intrinsic $name"
return@forEachIndexed
}
cppTypes += type
args += argName to type
}
val desc = it.getElementsByTag("description").firstOrNull()?.text()
if(desc == null) {
errors += "Missing description element for intrinsic $name"
return@forEachIndexed
}
val op = it.getElementsByTag("operation").firstOrNull()?.text()
val insn = mutableListOf<Triple<String, String, String?>>()
it.getElementsByTag("instruction").forEachIndexed { i, ins ->
val insnName = ins.attribute("xed")?.value ?: ins.attribute("name")?.value
if(insnName == null) {
errors += "Missing both xed and name attribute for instruction $i in intrinsic $name"
return@forEachIndexed
}
val insnMnemonic = ins.attribute("name")?.value
if(insnMnemonic == null) {
errors += "Missing name attribute for instruction $insnName in intrinsic $name"
return@forEachIndexed
}
val insnForm = ins.attribute("form")?.value
insn += Triple(insnName, insnMnemonic, insnForm)
}
val cpuid = it.getElementsByTag("cpuid").firstOrNull()?.text()
val category = it.getElementsByTag("category").firstOrNull()?.text()
if(category == null) {
errors += "Missing category element for intrinsic $name"
return@forEachIndexed
}
val intrinsic = XmlIntrinsic(name, tech, retType, retVar, args, desc, op, insn, cpuid, category)
intrins += intrinsic
techs += tech
cpuid?.let { c -> cpuids += c }
categories += category
cppTypes += retType
}
if(errors.isNotEmpty()) {
errors.forEach { System.err.println(it) }
throw Exception("XML file is (partially) invalid")
}
XmlData(types = cppTypes, techs = techs, cpuids = cpuids, categories = categories, intrinsics = intrins)
}
suspend fun loadJson(jsonFile: String): JsonData = coroutineScope {
val json = File(jsonFile).readText(Charsets.UTF_8)
val schema = JSONObject(json)
val pSet = mutableSetOf<String>()
val res = mutableMapOf<String, MutableMap<String, Performance>>()
schema.keys().forEach { opcode ->
val pMap = mutableMapOf<String, Performance>()
val platforms = schema.getJSONArray(opcode)
for (i in 0 until platforms.length()) {
val platform = platforms.getJSONObject(i)
platform.keys().forEach { k ->
pSet += k
val latency = platform.getJSONObject(k).getString("l").toFloatOrNull()
val throughput = platform.getJSONObject(k).getString("t").toFloatOrNull()
pMap += k to Performance(latency, throughput)
}
}
res += opcode to pMap
}
JsonData(pSet, res)
}
suspend fun importToDb(xml: XmlData, json: JsonData) = coroutineScope {
val db = Database.db
transaction {
val techMap = xml.techs.associateWith { tech -> Tech.new { name = tech } }
val typeMap = xml.types.associateWith { type -> CppType.new { name = type } }
val catMap = xml.categories.associateWith { cat -> Category.new { name = cat } }
val cpuidMap = xml.cpuids.associateWith { cpuid -> CPUID.new { name = cpuid } }
val platformMap = json.platforms.associateWith { platform -> Platform.new { name = platform } }
xml.intrinsics.forEach { intr ->
val dbIn = Intrinsic.new {
mnemonic = intr.name
returnType = typeMap[intr.retType] ?: throw Exception("Type ${intr.retType} not found")
returnVar = intr.retVar
description = intr.desc
operations = intr.op
category = catMap[intr.category] ?: throw Exception("Category ${intr.category} not found")
cpuid = intr.cpuid?.let { cpuidMap[it] ?: throw Exception("CPUID ${intr.cpuid} not found") }
tech = techMap[intr.tech] ?: throw Exception("Tech ${intr.tech} not found")
}
intr.args.forEachIndexed { i, arg ->
IntrinsicArgument.new {
intrinsic = dbIn
name = arg.first
type = typeMap[arg.second] ?: throw Exception("Type ${arg.second} not found")
index = i
}
}
intr.insn.forEach { insn ->
val dbInsn = IntrinsicInstruction.new {
intrinsic = dbIn
xed = insn.first
mnemonic = insn.second
insn.third?.let { form = it }
}
json.data[insn.first]?.forEach { (pl, perf) ->
val dbPl = platformMap[pl] ?: throw Exception("Platform $pl not found")
Performances.insert {
it[instruction] = dbInsn.id
it[platform] = dbPl.id
it[latency] = perf.latency
it[throughput] = perf.throughput
}
}
}
}
}
}
}

View File

@ -0,0 +1,63 @@
package com.jaytux.simd.data
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.dao.id.CompositeIdTable
object Techs : UUIDTable() {
val name = varchar("name", 255).uniqueIndex()
}
object CppTypes : UUIDTable() {
val name = varchar("name", 255).uniqueIndex()
}
object Categories : UUIDTable() {
val name = varchar("name", 255).uniqueIndex()
}
object CPUIDs : UUIDTable() {
val name = varchar("name", 255).uniqueIndex()
}
object Intrinsics : UUIDTable() {
val mnemonic = varchar("mnemonic", 255)
val returnType = reference("return_type", CppTypes)
val returnVar = varchar("return_var", 255).nullable()
val description = text("description")
val operations = text("operations").nullable()
val category = reference("category", Categories)
val cpuid = reference("cpuid", CPUIDs).nullable()
val tech = reference("tech", Techs)
}
object IntrinsicArguments : UUIDTable() {
val intrinsic = reference("intrinsic", Intrinsics)
val name = varchar("name", 255)
val type = reference("type", CppTypes)
val index = integer("index")
init {
uniqueIndex(intrinsic, name)
uniqueIndex(intrinsic, index)
}
}
object IntrinsicInstructions : UUIDTable() {
val intrinsic = reference("intrinsic", Intrinsics)
val mnemonic = varchar("mnemonic", 255)
val xed = varchar("xed", 255)
val form = text("form").nullable()
}
object Platforms : UUIDTable() {
val name = varchar("name", 255).uniqueIndex()
}
object Performances : CompositeIdTable() {
val instruction = reference("instruction", IntrinsicInstructions)
val platform = reference("platform", Platforms)
val latency = float("latency").nullable()
val throughput = float("throughput").nullable()
override val primaryKey: PrimaryKey = PrimaryKey(instruction, platform)
}

View File

@ -0,0 +1,122 @@
package com.jaytux.simd.server
import com.jaytux.simd.data.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.*
@Serializable
data class IntrinsicSummary(@Serializable(with = UUIDSerializer::class) val id: UUID, val name: String)
@Serializable
data class Param(val name: String, val type: String)
@Serializable
data class Instruction(val mnemonic: String, val xed: String, val form: String?)
@Serializable
data class PlatformPerformance(val platform: String, val latency: Float?, val throughput: Float?)
@Serializable
data class IntrinsicDetails(
@Serializable(with = UUIDSerializer::class) val id: UUID,
val name: String,
val returnType: String,
val returnVar: String?,
val description: String,
val operations: String?,
val category: String,
val cpuid: String?,
val tech: String,
val params: List<Param>,
val instructions: List<Instruction>?,
val performance: List<PlatformPerformance>?
)
fun Routing.installGetAll() {
getPagedUrl("/all", { 100 }, { IntrinsicSummary(it.id.value, it.mnemonic) }) {
Intrinsic.all().orderAsc(Intrinsics.mnemonic)
}
getPagedUrl("/cpuid", { 100 }, { it.name }) { CPUID.all().orderAsc(CPUIDs.name) }
getPagedUrl("/tech", { 100 }, { it.name }) { Tech.all().orderAsc(Techs.name) }
getPagedUrl("/category", { 100 }, { it.name }) { Category.all().orderAsc(Categories.name) }
getPagedUrl("/types", { 100 }, { it.name }) { CppType.all().orderAsc(CppTypes.name) }
}
fun Routing.installSearch() {
getPagedRequest("/search", { 100 }, { IntrinsicSummary(it[Intrinsics.id].value, it[Intrinsics.mnemonic]) }) {
val name = call.request.queryParameters["name"]
val returnType = call.request.queryParameters["return"]?.let {
CppType.find { CppTypes.name eq it }.firstOrNull()
?: throw HttpError("Unknown return type: $it")
}
val cpuid = call.request.queryParameters["cpuid"]?.let {
CPUID.find { CPUIDs.name eq it }.firstOrNull()
?: throw HttpError("Unknown CPUID: $it")
}
val tech = call.request.queryParameters["tech"]?.let {
Tech.find { Techs.name eq it }.firstOrNull()
?: throw HttpError("Unknown tech: $it")
}
val category = call.request.queryParameters["category"]?.let {
Category.find { Categories.name eq it }.firstOrNull()
?: throw HttpError("Unknown category: $it")
}
val desc = call.request.queryParameters["desc"]
var results = Intrinsics.selectAll()
name?.let { results = results.where { Intrinsics.mnemonic like "%$it%" } }
returnType?.let { results = results.where { Intrinsics.returnType eq it.id } }
cpuid?.let { results = results.where { Intrinsics.cpuid eq it.id } }
tech?.let { results = results.where { Intrinsics.tech eq it.id } }
category?.let { results = results.where { Intrinsics.category eq it.id } }
desc?.let { results = results.where { Intrinsics.description like "%$it%" } }
results.orderAsc(Intrinsics.mnemonic)
}
}
fun Routing.installDetails() {
get("/details/{id}") {
runCatching {
transaction {
val id = call.parameters["id"]?.let { UUID.fromString(it) }
?: throw HttpError("Missing or invalid ID")
val intrinsic = Intrinsic.findById(id)
?: throw HttpError("Unknown intrinsic: $id")
IntrinsicDetails(
id = intrinsic.id.value,
name = intrinsic.mnemonic,
returnType = intrinsic.returnType.name,
returnVar = intrinsic.returnVar,
description = intrinsic.description,
operations = intrinsic.operations,
category = intrinsic.category.name,
cpuid = intrinsic.cpuid?.name,
tech = intrinsic.tech.name,
params = intrinsic.arguments.orderAsc(IntrinsicArguments.index)
.map { Param(it.name, it.type.name) },
instructions = intrinsic.instructions.emptyToNull()
?.map { Instruction(it.mnemonic, it.xed, it.form) },
performance = intrinsic.instructions.firstOrNull()?.let {
(Performances innerJoin Platforms).selectAll().where {
Performances.instruction eq it.id
}.map {
PlatformPerformance(
platform = it[Platforms.name],
latency = it[Performances.latency],
throughput = it[Performances.throughput]
)
}
}
)
}
}
}
}

View File

@ -0,0 +1,10 @@
package com.jaytux.simd.server
import io.ktor.server.application.*
import io.ktor.server.routing.*
fun Application.configureHTTP() {
routing {
//
}
}

View File

@ -0,0 +1,18 @@
package com.jaytux.simd.server
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.Expression
import org.jetbrains.exposed.sql.SizedIterable
import org.jetbrains.exposed.sql.SortOrder
@Serializable data class Paginated<T>(val page: Long, val totalPages: Long, val items: List<T>)
inline fun <reified T, reified R> SizedIterable<T>.paginated(page: Long, perPage: Int, crossinline mapper: (T) -> R): Paginated<R> {
val total = this.count()
val subset = this.offset(page * perPage).limit(perPage).map { item -> mapper(item) }
return Paginated(page, total / perPage + if (total % perPage > 0) 1 else 0, subset)
}
fun <T> SizedIterable<T>.orderAsc(expr: Expression<*>) = this.orderBy(expr to SortOrder.ASC)
fun <T> SizedIterable<T>.emptyToNull() = if(empty()) null else this

View File

@ -0,0 +1,66 @@
package com.jaytux.simd.server
import com.jaytux.simd.data.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.autohead.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.*
@Serializable data class ErrorResponse(val error: String)
class HttpError(msg: String, val status: HttpStatusCode = HttpStatusCode.BadRequest) : Exception(msg)
inline suspend fun <reified R: Any> RoutingContext.runCatching(crossinline block: suspend RoutingContext.() -> R) {
try {
call.respond(block())
}
catch(err: HttpError) {
call.respond(err.status, ErrorResponse(err.message ?: "<no error message given>"))
}
}
inline fun <reified T, reified R> Route.getPagedUrl(
path: String,
crossinline perPage: RoutingContext.() -> Int,
crossinline mapper: (T) -> R,
crossinline getSet: RoutingContext.() -> SizedIterable<T>,
) {
get(path) {
runCatching {
transaction { getSet().paginated(0, perPage(), mapper) }
}
}
get("$path/{page}") {
runCatching {
val page = call.parameters["page"]?.toLongOrNull() ?: 0
transaction { getSet().paginated(page, perPage(), mapper) }
}
}
}
inline fun <reified T, reified R> Route.getPagedRequest(
path: String,
crossinline perPage: RoutingContext.() -> Int,
crossinline mapper: (T) -> R,
crossinline getSet: RoutingContext.() -> SizedIterable<T>,
) {
get(path) {
runCatching {
val page = call.request.queryParameters["page"]?.toLongOrNull() ?: 0
transaction { getSet().paginated(page, perPage(), mapper) }
}
}
}
fun Application.configureRouting() {
install(AutoHeadResponse)
routing {
installGetAll()
installSearch()
installDetails()
}
}

View File

@ -0,0 +1,28 @@
package com.jaytux.simd.server
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.routing.*
import io.ktor.util.*
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.Decoder
import java.util.*
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
}
object UUIDSerializer : KSerializer<UUID> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
override fun serialize(encoder: Encoder, value: UUID) = encoder.encodeString(value.toString())
}

View File

@ -0,0 +1,5 @@
ktor:
application:
modules: [ com.jaytux.simd.MainKt.module ]
deployment:
port: 42024

View File

@ -0,0 +1,10 @@
<configuration>
<appender name="APPENDER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="trace">
<appender-ref ref="APPENDER"/>
</root>
</configuration>