FusionAds-iOS/FusionAds/Classes/fusion/engine/mab/base/MabAuctioneer.swift

545 lines
15 KiB
Swift
Raw Normal View History

import Foundation
// AggregatorBid
private struct AggregatorBid {
let config: AggregatorConfig
/**
*
* show revenue * selfRaise bid
* true[floor]revenue
*/
let useSelfRaise: Bool
/**
* bidnil使winnersrevenue, see [MabWinners.maxRevenue]
*
*/
let floor: Double?
/**
*
*/
let reason: RequestReason?
init(config: AggregatorConfig, useSelfRaise: Bool = false, floor: Double? = nil, reason: RequestReason? = nil) {
self.config = config
self.useSelfRaise = useSelfRaise
self.floor = floor
self.reason = reason
}
var raiseRate: Double {
return useSelfRaise ? config.selfRaiseRate ?? config.raiseRate : config.raiseRate
}
}
//
extension AggregatorConfig {
var deferRetryUntilShown: Bool {
return adPlatform == AdPlatform.adMob.name
}
}
/**
* 广
*/
private class MabWinners<T: MabBidder> {
private var winners: [String: T] = [:]
private let onEvict: ((T) -> Void)?
init(onEvict: ((T) -> Void)? = nil) {
self.onEvict = onEvict
}
/**
*
*/
func evictCache() {
let expiredKeys = winners.filter { (_, bidder) in
if bidder.isExpired != false {
onEvict?(bidder)
return true
}
return false
}.map { $0.key }
for key in expiredKeys {
winners.removeValue(forKey: key)
}
}
/**
* 广
*/
func add(bidder: T) {
evictCache()
winners[bidder.aggregatorId] = bidder
}
/**
* 广
*/
func contains(aggregatorId: String) -> Bool {
evictCache()
return winners[aggregatorId] != nil
}
/**
* 广广0
*/
func maxRevenue() -> Double {
evictCache()
if winners.isEmpty {
return 0.0
}
return winners.values.map { $0.revenue }.max() ?? 0.0
}
/**
* 广
*/
func peekMaxRevenue() -> T? {
evictCache()
return winners.values.max(by: {
if($0.revenue == $1.revenue) {
//
return $0.config.priority > $1.config.priority
}
return $0.revenue < $1.revenue
})
}
/**
* 广
*/
func pollMaxRevenue() -> T? {
evictCache()
guard let entry = peekMaxRevenue() else {
return nil
}
return winners.removeValue(forKey: entry.aggregatorId)
}
/**
* 广
*/
func isEmpty() -> Bool {
evictCache()
return winners.isEmpty
}
/**
* 广
*/
func clear() {
winners.removeAll()
}
func buildStateLog() -> String {
var result = "winners["
for winner in winners {
result += "\(winner.key)(\(winner.value.revenue)),"
}
result += "]"
return result
}
}
/**
* loadAdMobloadloadloadload广show
* -> "每次广告show完此聚合重新获得一次load机会不能累加"
*/
private class RetryGate {
// SwiftAtomicInteger使
private class AtomicInteger {
private var value: Int
init(_ initialValue: Int = 0) {
self.value = initialValue
}
func get() -> Int {
return value
}
func set(_ newValue: Int) {
value = newValue
}
func incrementAndGet() -> Int {
value += 1
return value
}
}
private var version = AtomicInteger() // ad show
private var lastUsed: [String: AtomicInteger] = [:]
func onAdShown() {
_ = version.incrementAndGet()
}
/**
* showretryshow
*/
func canRetry(aggregatorId: String) -> Bool {
return getLastUsed(aggregatorId: aggregatorId).get() < version.get()
}
/**
*
*/
func markRetryUsed(aggregatorId: String) {
getLastUsed(aggregatorId: aggregatorId).set(version.get())
}
private func getLastUsed(aggregatorId: String) -> AtomicInteger {
if let lastUsed = lastUsed[aggregatorId] {
return lastUsed
} else {
let atomicInteger = AtomicInteger(-1)
lastUsed[aggregatorId] = atomicInteger
return atomicInteger
}
}
}
private func selectAdUnitSpec(
aggregatorConfig: AggregatorConfig,
floor: Double,
selfRaise: Bool = false
) -> AdUnitSpec? {
let adUnitIds = aggregatorConfig.adUnitIds
if adUnitIds.isEmpty {
return nil
}
let targetFloor = floor * (selfRaise ? aggregatorConfig.selfRaiseRate ?? aggregatorConfig.raiseRate : aggregatorConfig.raiseRate)
let firstGtIndex = adUnitIds.firstIndex { $0.floor >= targetFloor }
if firstGtIndex == nil {
return adUnitIds.last
}
if firstGtIndex == 0 {
return adUnitIds.first
}
// targetFloor
let index = firstGtIndex!
let nearestIndex = (abs(targetFloor - adUnitIds[index].floor) <= abs(adUnitIds[index - 1].floor - targetFloor)) ? index : index - 1
let selection = adUnitIds[nearestIndex]
return selection
}
enum RequestReason: String {
case INIT = "init"
case FAIL = "fail"
case SHOW = "show"
case EXPIRATION = "exp"
var statsName: String {
return rawValue
}
}
protocol MabBidder {
var id: Int { get }
var config: AggregatorConfig { get }
var isExpired: Bool? { get }
var revenue: Double { get }
func load(reason: RequestReason?) -> Bool
func destroy()
}
extension MabBidder {
var aggregatorId: String {
return config.aggregatorId
}
}
//
class MabBidderUtils {
static func adjustRevenue(ad: FusionAd, floor: Double) -> Double {
switch ad.adPlatform {
case .max:
return ad.revenue
case .ironSource:
return max(ad.revenue, floor)
case .adMob:
return max(ad.revenue, floor)
default:
return max(ad.revenue, floor)
}
}
}
class MabAuctioneer<T: MabBidder> {
enum RequestResult {
case success
case busy
case noAvailableSource
}
private let tag: String
private let faUnitId: String
private let priorityAggregators: [AggregatorConfig]
private var candidatesQueue: [AggregatorBid] = []
private let bidderFactory: (AggregatorConfig, AdUnitSpec) -> T?
private var bidding: T?
private var bidders: [String: T] = [:]
private let winners: MabWinners<T>
private let retryGate = RetryGate()
var hasWinner: Bool {
return !winners.isEmpty()
}
init(
engineId: Int,
faUnitId: String,
aggregatorConfigs: [AggregatorConfig],
bidderFactory: @escaping (AggregatorConfig, AdUnitSpec) -> T?
) {
self.tag = "MabAuctioneer@\(engineId)"
self.faUnitId = faUnitId
self.priorityAggregators = aggregatorConfigs.sorted(by: { $0.priority < $1.priority })
self.bidderFactory = bidderFactory
self.winners = MabWinners<T>()
}
private func logI(_ message: String) {
Logger.i(tag: tag, message: message)
}
private func logD(_ message: String) {
Logger.d(tag: tag, message: message)
}
private func logW(_ message: String) {
Logger.w(tag: tag, message: message)
}
private func logE(_ message: String) {
Logger.e(tag: tag, message: message)
}
private func logState() {
logI("mab state: \(buildStateLog())")
}
private func obtainBidder(
config: AggregatorConfig,
adUnitSpec: AdUnitSpec
) -> T? {
if let bidder = bidders[adUnitSpec.adUnitId] {
return bidder
} else {
if let newBidder = bidderFactory(config, adUnitSpec) {
bidders[adUnitSpec.adUnitId] = newBidder
return newBidder
}
return nil
}
}
func requestNext() -> RequestResult {
logI("requestNext")
let result = internalRequestNext()
logI("requestNext result \(result)")
logState()
return result
}
private func internalRequestNext() -> RequestResult {
if bidding != nil {
return .busy
}
while !candidatesQueue.isEmpty {
guard let bid = candidatesQueue.first else {
break
}
candidatesQueue.removeFirst()
if winners.contains(aggregatorId: bid.config.aggregatorId) {
// bid
continue
}
guard let adUnitSpec = selectAdUnitSpec(
aggregatorConfig: bid.config,
floor: bid.floor ?? winners.maxRevenue(),
selfRaise: bid.useSelfRaise
) else {
continue
}
logD("selected \(adUnitSpec.adUnitId) with \(adUnitSpec.floorEcpm)")
if let bidder = obtainBidder(config: bid.config, adUnitSpec: adUnitSpec) {
if bidder.load(reason: bid.reason) {
bidding = bidder
return .success
}
}
}
return .noAvailableSource
}
func refillCandidatesQueue(reason: RequestReason? = nil) {
logI("refilling candidates")
logState()
for aggregator in priorityAggregators {
fillCandidate(AggregatorBid(config: aggregator, useSelfRaise: false, reason: reason))
}
logI("candidates refilled")
logState()
}
private func fillCandidate(_ bid: AggregatorBid) {
let aggregator = bid.config
//
if winners.contains(aggregatorId: aggregator.aggregatorId) {
logD("refilling - \(aggregator.aggregatorId) filled, skip")
return
}
//
if let currentBidding = bidding, currentBidding.aggregatorId == aggregator.aggregatorId {
logW("refilling - \(aggregator.aggregatorId) is loading, skip")
return
}
// show
if aggregator.deferRetryUntilShown {
if !retryGate.canRetry(aggregatorId: aggregator.aggregatorId) {
logD("refilling - \(aggregator.aggregatorId) deferred until next shown")
return
}
retryGate.markRetryUsed(aggregatorId: aggregator.aggregatorId)
}
logD("refilling - \(aggregator.aggregatorId) added to queue")
candidatesQueue.append(bid)
}
private func onCacheEvict(_ bidder: T) {
// candidatesQueue.append(AggregatorBid(config: bidder.config, reason: .EXPIRATION))
}
func reset() {
logI("reset")
for bidder in bidders.values {
bidder.destroy()
}
bidding?.destroy()
bidding = nil
winners.clear()
bidders.removeAll()
candidatesQueue.removeAll()
refillCandidatesQueue()
}
func onShown(winner: T) {
logI("winner shown, \(winner)")
logState()
retryGate.onAdShown()
// hidden
candidatesQueue.removeAll()
//
if winner.config.selfRaiseRate != nil {
// show
candidatesQueue.append(
AggregatorBid(
config: winner.config,
useSelfRaise: true,
floor: winner.revenue,
reason: .SHOW
)
)
}
}
func onLoaded(bidderId: Int) {
logI("onLoaded \(bidderId)")
if let bidding = bidding, bidding.id == bidderId {
winners.add(bidder: bidding)
self.bidding = nil
} else {
logW("onLoaded bidderId mismatch \(String(describing: bidding?.id)) != \(bidderId)")
}
logState()
}
func onLoadFailed(bidderId: Int) {
logI("onLoadFailed \(bidderId)")
if let bidder = bidding, bidder.id == bidderId {
bidding = nil
} else {
logW("onLoadFailed bidderId mismatch \(String(describing: bidding?.id)) != \(bidderId)")
}
logState()
}
func pollMaxRevenue() -> T? {
return winners.pollMaxRevenue()
}
func peekMaxRevenue() -> T? {
return winners.peekMaxRevenue()
}
func evictCache() {
winners.evictCache()
}
private func buildStateLog() -> String {
var result = "faUnitId(\(faUnitId)), "
result += winners.buildStateLog()
result += ", "
result += "bidding["
if let bidding = bidding {
result += "\(bidding)"
} else {
result += "null"
}
result += "], "
result += "candidates["
for bid in candidatesQueue {
result += "\(bid.config.aggregatorId)("
result += "\(bid.raiseRate)"
if bid.useSelfRaise {
result += " self-raise"
}
result += "), "
}
result += "]"
return result
}
}